diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..2f6f88d0df --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,13 @@ +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 \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 + +# PHP memory limit +RUN echo "memory_limit=768M" > /usr/local/etc/php/php.ini + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ff13dd64a3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +{ + "name": "PHP", + "build": { + "dockerfile": "Dockerfile" + }, + + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "felixfbecker.php-debug", + "felixfbecker.php-intellisense", + "mrmlnc.vscode-apache" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8080], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "composer install" + + // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} diff --git a/.devcontainer/library-scripts/README.md b/.devcontainer/library-scripts/README.md new file mode 100644 index 0000000000..d06dfd1a95 --- /dev/null +++ b/.devcontainer/library-scripts/README.md @@ -0,0 +1,5 @@ +# Warning: Folder contents may be replaced + +The contents of this folder will be automatically replaced with a file of the same name in the [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers) repository's [script-library folder](https://github.com/microsoft/vscode-dev-containers/tree/master/script-library) whenever the repository is packaged. + +To retain your edits, move the file to a different location. You may also delete the files if they are not needed. \ No newline at end of file diff --git a/.devcontainer/library-scripts/common-debian.sh b/.devcontainer/library-scripts/common-debian.sh new file mode 100755 index 0000000000..20a9216af7 --- /dev/null +++ b/.devcontainer/library-scripts/common-debian.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"vscode"} +USER_UID=${3:-1000} +USER_GID=${4:-1000} +UPGRADE_PACKAGES=${5:-"true"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Treat a user name of "none" as root +if [ "${USERNAME}" = "none" ] || [ "${USERNAME}" = "root" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt-get-update-if-needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + apt-get-update-if-needed + + PACKAGE_LIST="apt-utils \ + git \ + openssh-client \ + gnupg2 \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust0 \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db" + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + PACKAGE_LIST="${PACKAGE_LIST} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${PACKAGE_LIST}" + apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt-get-update-if-needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ]; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user. +if id -u $USERNAME > /dev/null 2>&1; then + # User exists, update if needed + if [ "$USER_GID" != "$(id -G $USERNAME)" ]; then + groupmod --gid $USER_GID $USERNAME + usermod --gid $USER_GID $USERNAME + fi + if [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + groupadd --gid $USER_GID $USERNAME + useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME +fi + +# Add add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# .bashrc/.zshrc snippet +RC_SNIPPET="$(cat << EOF +export USER=\$(whoami) + +export PATH=\$PATH:\$HOME/.local/bin + +if type code-insiders > /dev/null 2>&1 && ! type code > /dev/null 2>&1; then + alias code=code-insiders +fi +EOF +)" + +# Ensure ~/.local/bin is in the PATH for root and non-root users for bash. (zsh is later) +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${RC_SNIPPET}" >> /etc/bash.bashrc + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh +if [ "${INSTALL_ZSH}" = "true" ] && [ ! -d "/root/.oh-my-zsh" ] && [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + apt-get-update-if-needed + apt-get install -y zsh + curl -fsSLo- https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh | bash 2>&1 + echo "${RC_SNIPPET}" >> /etc/zsh/zshrc + echo -e "DEFAULT_USER=\$USER\nprompt_context(){}" >> /root/.zshrc + cp -fR /root/.oh-my-zsh /etc/skel + cp -f /root/.zshrc /etc/skel + sed -i -e "s/\/root\/.oh-my-zsh/\/home\/\$(whoami)\/.oh-my-zsh/g" /etc/skel/.zshrc + if [ "${USERNAME}" != "root" ]; then + cp -fR /etc/skel/.oh-my-zsh /etc/skel/.zshrc /home/$USERNAME + chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc + fi + ZSH_ALREADY_INSTALLED="true" +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" \ No newline at end of file diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh new file mode 100644 index 0000000000..d230a14e82 --- /dev/null +++ b/.devcontainer/library-scripts/node-debian.sh @@ -0,0 +1,105 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] + +export NVM_DIR=${1:-"/usr/local/share/nvm"} +export NODE_VERSION=${2:-"lts/*"} +USERNAME=${3:-"vscode"} +UPDATE_RC=${4:-"true"} + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# 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 + +if [ "${NODE_VERSION}" = "none" ]; then + export NODE_VERSION= +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install curl, apt-transport-https, tar, or gpg if missing +if ! dpkg -s apt-transport-https curl ca-certificates tar > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + apt-get update + fi + apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates tar gnupg2 +fi + +# Install yarn +if type yarn > /dev/null 2>&1; then + echo "Yarn already installed." +else + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | (OUT=$(apt-key add - 2>&1) || echo $OUT) + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + apt-get update + apt-get -y install --no-install-recommends yarn +fi + +# Install the specified node version if NVM directory already exists, then exit +if [ -d "${NVM_DIR}" ]; then + echo "NVM already installed." + if [ "${NODE_VERSION}" != "" ]; then + su ${USERNAME} -c "source $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" + fi + exit 0 +fi + + +# Run NVM installer as non-root if needed +mkdir -p ${NVM_DIR} +chown ${USERNAME} ${NVM_DIR} +su ${USERNAME} -c "$(cat << EOF + set -e + + # Do not update profile - we'll do this manually + export PROFILE=/dev/null + + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash + source ${NVM_DIR}/nvm.sh + if [ "${NODE_VERSION}" != "" ]; then + nvm alias default ${NODE_VERSION} + fi + nvm clear-cache +EOF +)" 2>&1 + +if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc with NVM scripts..." +(cat < /dev/null 2>&1; then + echo "Fixing permissions of \"\$NVM_DIR\"..." + sudoIf chown -R ${USERNAME}:root \$NVM_DIR + else + echo "Warning: NVM directory is not owned by ${USERNAME} and sudo is not installed. Unable to correct permissions." + fi +fi +[ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh" +[ -s "\$NVM_DIR/bash_completion" ] && . "\$NVM_DIR/bash_completion" +EOF +) | tee -a /etc/bash.bashrc >> /etc/zsh/zshrc +fi + +echo "Done!" \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 5d66bc427b..0dc4814a91 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,11 @@ insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true -[*.{php,phpt}] +[*.{php,phpt,stub}] +indent_style = tab +indent_size = 4 + +[bin/phpstan] indent_style = tab indent_size = 4 diff --git a/.gitattributes b/.gitattributes index 38d1f155b8..ed98b8a4c8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,4 @@ *.php text eol=lf *.stub linguist-language=PHP -*.neon linguist-language=YAML tests/PHPStan/Command/ErrorFormatter/data/WindowsNewlines.php eol=crlf diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 3f6be2dabb..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: -- package-ecosystem: composer - directory: "/build-cs" - schedule: - interval: daily - open-pull-requests-limit: 10 -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..59522a2dbc --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,51 @@ +{ + "extends": [ + "config:base", + "schedule:weekly" + ], + "dependencyDashboard": true, + "rangeStrategy": "update-lockfile", + "rebaseWhen": "conflicted", + "baseBranches": ["2.1.x"], + "packageRules": [ + { + "matchPackagePatterns": ["*"], + "enabled": false + }, + { + "matchPaths": ["+(composer.json)"], + "enabled": true, + "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, + "groupName": "compiler" + }, + { + "matchPaths": [".github/**"], + "enabled": true, + "groupName": "github-actions" + } + ] +} 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 new file mode 100644 index 0000000000..541d15addc --- /dev/null +++ b/.github/workflows/backward-compatibility.yml @@ -0,0 +1,47 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Backward Compatibility" + +on: + pull_request: + push: + branches: + - "2.1.x" + paths: + - 'src/**' + - '.github/workflows/backward-compatibility.yml' + +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: + name: "Backward Compatibility" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + + - name: "Install dependencies" + run: "composer install --no-dev --no-interaction --no-progress" + + - name: "Install BackwardCompatibilityCheck" + run: | + composer global config minimum-stability dev + composer global config prefer-stable true + composer global require --dev ondrejmirtes/backward-compatibility-check:^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/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 69f8591abc..0000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,482 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -name: "Build" - -on: - pull_request: - push: - branches: - - "master" - -env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" - -jobs: - lint: - name: "Lint" - - runs-on: "ubuntu-latest" - - strategy: - matrix: - php-version: - - "7.1" - - "7.2" - - "7.3" - - "7.4" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: "Validate Composer" - run: "composer validate" - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - if: matrix.php-version != '7.4' - run: php bin/transform-source.php - - - name: "Lint" - run: "vendor/bin/phing lint" - - coding-standards: - name: "Coding Standard" - - runs-on: "ubuntu-latest" - - strategy: - matrix: - php-version: - - "7.4" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: "Validate Composer" - run: "composer validate" - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - if: matrix.php-version != '7.4' - run: php bin/transform-source.php - - - name: "Composer Normalize" - run: "vendor/bin/phing composer-normalize-check" - - - name: "Lint" - run: "vendor/bin/phing lint" - - - name: "Coding Standard" - run: "vendor/bin/phing cs" - - dependency-analysis: - name: "Dependency Analysis" - - runs-on: "ubuntu-latest" - - strategy: - matrix: - php-version: - - "7.4" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Composer Require Checker" - run: "vendor/bin/phing composer-require-checker" - - tests: - name: "Tests" - - runs-on: ${{ matrix.operating-system }} - - strategy: - fail-fast: false - matrix: - php-version: - - "7.1" - - "7.2" - - "7.3" - - "7.4" - operating-system: [ubuntu-latest, windows-latest] - script: - - "vendor/bin/phing tests" - - "vendor/bin/phing tests-fast-static-reflection" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - tools: pecl - extensions: ds,mbstring - ini-values: memory_limit=512M - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - if: matrix.php-version != '7.4' - run: php bin/transform-source.php - - - name: "Tests" - run: "${{ matrix.script }}" - - tests-code-coverage: - name: "Tests with code coverage" - - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "pcov" - php-version: "7.4" - tools: pecl - extensions: ds - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-7.4-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-7.4-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Tests" - run: | - composer require pcov/clobber --dev && \ - vendor/bin/pcov clobber && \ - php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude="~vendor~" vendor/bin/phpunit -c tests/phpunit.xml tests/PHPStan - - - name: "Codecov.io" - env: - CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" - run: "bash <(curl -s https://codecov.io/bash) -f tests/tmp/clover.xml" - - static-analysis: - name: "PHPStan" - - runs-on: ${{ matrix.operating-system }} - - strategy: - fail-fast: false - matrix: - php-version: - - "7.1" - - "7.2" - - "7.3" - - "7.4" - operating-system: [ubuntu-latest, windows-latest] - script: - - "vendor/bin/phing phpstan" - - "vendor/bin/phing phpstan-runtime-reflection" - - "vendor/bin/phing phpstan-static-reflection" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - extensions: mbstring - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - if: matrix.php-version != '7.4' - run: php bin/transform-source.php - - - name: "PHPStan" - run: ${{ matrix.script }} - - static-analysis-with-result-cache: - name: "PHPStan with result cache" - - runs-on: ${{ matrix.operating-system }} - - strategy: - matrix: - php-version: - - "7.4" - operating-system: [ubuntu-latest, windows-latest] - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - extensions: mbstring - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Cache Result cache" - uses: actions/cache@v1 - with: - path: ./tmp - key: "result-cache-v4" - - - name: "PHPStan with result cache" - if: matrix.operating-system == 'ubuntu-latest' - run: | - vendor/bin/phing phpstan-result-cache - bin/phpstan clear-result-cache -c build/phpstan.neon - vendor/bin/phing phpstan-result-cache - echo -e "\n\n" >> src/TrinaryLogic.php - vendor/bin/phing phpstan-result-cache - vendor/bin/phing phpstan-result-cache - - - name: "PHPStan with result cache" - if: matrix.operating-system == 'windows-latest' - shell: cmd - run: vendor\bin\phing phpstan-result-cache && php bin\phpstan clear-result-cache -c build/phpstan.neon && vendor\bin\phing phpstan-result-cache && echo. >> src\TrinaryLogic.php && vendor\bin\phing phpstan-result-cache && vendor\bin\phing phpstan-result-cache - - - name: "Upload result cache artifact" - uses: actions/upload-artifact@v2.0.1 - with: - name: resultCache-${{ matrix.operating-system }}.php - path: tmp/resultCache.php - - result-cache-e2e-tests: - name: "Result cache E2E tests" - - runs-on: ${{ matrix.operating-system }} - - strategy: - matrix: - php-version: - - "7.4" - operating-system: [ubuntu-latest, windows-latest] - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - extensions: mbstring - ini-values: memory_limit=256M - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - 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 -c tests/phpunit.xml tests/e2e/ResultCacheEndToEndTest.php - - compiler-tests: - name: "Compiler Tests" - - runs-on: "ubuntu-latest" - - strategy: - matrix: - php-version: - - "7.3" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-dev --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - run: php bin/transform-source.php - - - 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 - - generate-baseline: - name: "Generate baseline" - - runs-on: "ubuntu-latest" - - strategy: - matrix: - php-version: - - "7.4" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Generate baseline" - run: | - cp phpstan-baseline.neon phpstan-baseline-orig.neon && \ - vendor/bin/phing phpstan-generate-baseline && \ - diff phpstan-baseline.neon phpstan-baseline-orig.neon - - e2e-tests: - name: "E2E tests" - runs-on: "ubuntu-latest" - - strategy: - matrix: - include: - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php 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" - extensions: "soap" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" - extensions: "" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.2.0" - - - name: "Install PHP" - uses: "shivammathur/setup-php@2.3.0" - with: - coverage: "none" - php-version: "7.4" - tools: ${{ matrix.tools }} - extensions: ${{ matrix.extensions }} - - - name: "Cache dependencies" - uses: "actions/cache@v1.1.2" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-" - - - name: "Install dependencies" - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Test" - 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/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 new file mode 100644 index 0000000000..e02de443a5 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,383 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "E2E Tests" + +on: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + push: + branches: + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + +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: ubuntu-latest + + strategy: + fail-fast: false + matrix: + 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@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + extensions: mbstring + ini-values: memory_limit=256M + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - 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" + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + strategy: + matrix: + include: + - 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 -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 -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@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + 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 new file mode 100644 index 0000000000..d93e59843f --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,125 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Lint" + +on: + pull_request: + push: + branches: + - "2.1.x" + +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: + name: "Lint" + 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: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Validate Composer" + run: "composer validate" + + - 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' + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + + - name: "Lint" + run: "make lint" + + coding-standards: + name: "Coding Standard" + + 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: "Validate Composer" + run: "composer validate" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Lint" + run: "make lint" + + - name: "Coding Standard" + run: "make cs" + + dependency-analysis: + name: "Dependency Analysis" + + 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: "Composer Dependency Analyser" + run: "make composer-dependency-analyser" + + name-collision: + name: "Name Collision Detector" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.4" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Name Collision Detector" + run: "make name-collision" diff --git a/.github/workflows/merge-bot-pr.yml b/.github/workflows/merge-bot-pr.yml new file mode 100644 index 0000000000..6d34bb3d80 --- /dev/null +++ b/.github/workflows/merge-bot-pr.yml @@ -0,0 +1,29 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions +# https://github.com/WyriHaximus/github-action-wait-for-status + +name: Merge bot PR +on: + pull_request: + types: + - opened +jobs: + automerge: + name: Automerge PRs + runs-on: ubuntu-latest + steps: + - name: 'Wait for status checks' + if: github.event.pull_request.user.login == 'phpstan-bot' + id: waitforstatuschecks + uses: "WyriHaximus/github-action-wait-for-status@v1" + with: + ignoreActions: "automerge,Automerge PRs" + checkInterval: 13 + env: + GITHUB_TOKEN: "${{ secrets.PHPSTAN_BOT_TOKEN }}" + - name: Merge Pull Request + uses: juliangruber/merge-pull-request-action@v1 + if: steps.waitforstatuschecks.outputs.status == 'success' + with: + github-token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" + number: "${{ github.event.number }}" + method: rebase diff --git a/.github/workflows/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 new file mode 100644 index 0000000000..0cf91034a3 --- /dev/null +++ b/.github/workflows/phar.yml @@ -0,0 +1,247 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Compile PHAR" + +on: + pull_request: + push: + branches: + - "2.1.x" + tags: + - '2.1.*' + +concurrency: + group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests + cancel-in-progress: true + +jobs: + compiler-tests: + name: "Compiler Tests" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + outputs: + checksum: ${{ steps.checksum.outputs.md5 }} + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - 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" + + - 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" + 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: "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: + 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: "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@v4 + with: + repository: phpstan/phpstan + path: phpstan-dist + 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: "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: ${{ steps.import-gpg.outputs.fingerprint }} + + - name: "Verify PHAR" + working-directory: phpstan-dist + run: "gpg --verify phpstan.phar.asc" + + - name: "Install lucky_commit" + uses: baptiste0928/cargo-install@v3 + with: + crate: lucky_commit + args: --no-default-features + + - name: "Commit PHAR - development" + if: "!startsWith(github.ref, 'refs/tags/') && steps.checksum-difference.outputs.result == 'different'" + working-directory: phpstan-dist + env: + INPUT_LOG: ${{ steps.git-log.outputs.log }} + run: | + git config --global user.name "phpstan-bot" + git config --global user.email "ondrej+phpstanbot@mirtes.cz" + git add . + git commit --gpg-sign -m "Updated PHPStan to commit ${{ github.event.after }}" -m "$INPUT_LOG" --author "phpstan-bot " + lucky_commit ${{ steps.short-src-sha.outputs.sha }} + git push + + - name: "Commit PHAR - tag" + if: "startsWith(github.ref, 'refs/tags/')" + uses: stefanzweifel/git-auto-commit-action@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 new file mode 100644 index 0000000000..602152e12f --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,159 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Static Analysis" + +on: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + push: + branches: + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + +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: + name: "PHPStan" + runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + operating-system: [ubuntu-latest, windows-latest] + + steps: + - name: "Checkout" + 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: "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: "PHPStan" + run: "make phpstan" + + static-analysis-with-result-cache: + name: "PHPStan with result cache" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php-version: + - "8.1" + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: "Checkout" + 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@v4 + with: + path: ./tmp + key: "result-cache-v14-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v14-${{ matrix.php-version }}- + + - name: "PHPStan with result cache" + run: | + make phpstan-result-cache + bin/phpstan clear-result-cache -c build/phpstan.neon + make phpstan-result-cache + echo -e "\n\n" >> src/TrinaryLogic.php + make phpstan-result-cache + make phpstan-result-cache + + generate-baseline: + name: "Generate 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: | + 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 new file mode 100644 index 0000000000..d7c4673b40 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,156 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Tests" + +on: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + push: + branches: + - "2.1.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + +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: + name: "Tests" + runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + operating-system: [ ubuntu-latest, windows-latest ] + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - 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: "Tests" + run: "make tests" + + tests-integration: + name: "Integration tests" + runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + operating-system: [ ubuntu-latest, windows-latest ] + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=1G + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Tests" + run: "make tests-integration" + + tests-levels-matrix: + name: "Determine levels tests matrix" + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=1G + + - name: "Install PHPUnit 10.x" + run: "composer remove --dev brianium/paratest && composer require --dev --with-all-dependencies phpunit/phpunit:^10" + + - id: set-matrix + run: echo "matrix=$(php .github/workflows/tests-levels-matrix.php)" >> $GITHUB_OUTPUT + + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + tests-levels: + needs: tests-levels-matrix + + name: "Levels tests" + runs-on: ubuntu-latest + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + script: "${{fromJson(needs.tests-levels-matrix.outputs.matrix)}}" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=1G + + - name: "Install 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 new file mode 100644 index 0000000000..396be1c0be --- /dev/null +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -0,0 +1,50 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Update PhpStorm stubs" +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '0 0 * * 2' + +jobs: + update-phpstorm-stubs: + name: "Update PhpStorm stubs" + if: ${{ github.repository == 'phpstan/phpstan-src' }} + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + 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.1" + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + - name: "Checkout stubs" + uses: actions/checkout@v4 + with: + path: "phpstorm-stubs" + repository: "jetbrains/phpstorm-stubs" + - name: "Update stubs" + run: "composer require jetbrains/phpstorm-stubs:dev-master#$(git -C phpstorm-stubs rev-parse HEAD)" + - name: "Remove stubs repo" + run: "rm -r phpstorm-stubs" + - name: "Update function metadata" + run: "./bin/generate-function-metadata.php" + - name: "Create Pull Request" + id: create-pr + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + branch-suffix: random + delete-branch: true + title: "Update PhpStorm stubs" + body: "Update PhpStorm stubs" + committer: "phpstan-bot " + commit-message: "Update PhpStorm stubs" diff --git a/.gitignore b/.gitignore index 0e259031a0..47f19ba656 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ -/build/phpstan-generated.neon -/compiler/composer.lock +/phpstan.neon /compiler/tmp /compiler/vendor -/composer.lock /conf/config.local.yml /vendor -/.idea +/.idea/* +!.idea/icon.png /tests/tmp /tests/.phpunit.result.cache +/tests/PHPStan/Reflection/data/golden/ +tmp/.memory_limit +e2e/bashunit diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000..5f346e71c1 Binary files /dev/null and b/.idea/icon.png differ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8bf65b9d59..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -language: php - -stages: - - test - - name: phar - if: (branch = master OR tag IS present) && type = push - -env: - global: - secure: "EpvZZ1v6JvefnfhgYm3Y2WprJxjgr6zDw2FJs+WISEtd1PHJToFJOE59vW7DRTcr5ycR4jrHyANqqIJWbH1k3A3wuyavmkissNKHbFK6vmLtAC9TuI/x5zBd+/T5yQ6i6uBe43nDzbkrQDYtGyNMdn1FXhWV9Z/hNCZ6uD0aiO5+d49NFJoexUOt3+LCxrZAGCCsm49KYRff/62QxO2Wajlfdhx+PLO0igY/i9N3oUQoVfbBEbP1ZSAPLv7ZkZGL4XdMMYEGYqnOeMWk39MYID34RmCYteWRfED4oqYbi4rzOpW8YeA/YkuHGThIykSLBrjdAfwUpekVEAI9r1gdrh91Gkpm/W/trFygdfI2gqev5GVjbYgmKQMm50l1W8wiD+Tb+AMUIttEXGjgwd+K2rn1RBHjM+CjPEWWdppg/7OOYVIJg0gIr94TN2LCQWDfFN5SxIIf0BpQmWteGEPCDpxCc3jsjpaVFXQ2jrui69Pdjr8/u7XCisQD9zpn4sQ43GZkdHC4rGOoBrjXQDWMB/LZyYNymJ6fkkuceqSgn6vDyBEkp9UBR1CIv4P8Ray86qEPodDFbPZMVX2JqDwUHMH3HVl4FINPYtVW3/VNUK7VihKd33+AjoX7anRTeq0T8jXUT4IF6tAxbO4DaDBB4XjQ3vCBDH15WxwIxy81KKA=" - -before_script: - - if php --ri xdebug >/dev/null; then phpenv config-rm xdebug.ini; fi - - composer update --no-interaction - -jobs: - include: - - stage: phar - php: 7.3 - os: linux - dist: xenial - before_install: - - | - openssl aes-256-cbc -K $encrypted_bd816b4f73f9_key -iv $encrypted_bd816b4f73f9_iv -in build/key.gpg.enc -out build/key.gpg -d && \ - gpg --batch --import build/key.gpg && \ - rm build/key.gpg - script: - - | - composer install --working-dir=compiler && \ - php compiler/bin/compile && \ - GIT_LOG=$(git log ${TRAVIS_COMMIT_RANGE} --reverse --pretty='%H %s' | sed -e 's/^/https:\/\/github.com\/phpstan\/phpstan-src\/commit\//') && \ - git clone https://${GITHUB_TOKEN}@github.com/phpstan/phpstan.git phpstan-dist > /dev/null 2>&1 && \ - cp tmp/phpstan.phar phpstan-dist/phpstan.phar && \ - cp tmp/phpstan.phar phpstan-dist/phpstan && \ - cd phpstan-dist && \ - git config user.email "ondrej@mirtes.cz" && \ - git config user.name "Ondrej Mirtes" && \ - git config --global user.signingkey CF1A108D0E7AE720 && \ - rm phpstan.phar.asc && \ - gpg --batch -ab phpstan.phar && \ - gpg --verify phpstan.phar.asc && \ - git add phpstan phpstan.phar phpstan.phar.asc - - if [ "${TRAVIS_TAG}" != "" ]; then - COMMIT_MSG="PHPStan ${TRAVIS_TAG}" - else - COMMIT_MSG="Updated PHPStan to commit ${TRAVIS_COMMIT}" - fi - - git commit -S -m "${COMMIT_MSG}" -m "${GIT_LOG}" && \ - git push --quiet origin master - - if [ "${TRAVIS_TAG}" != "" ]; then - git tag -s ${TRAVIS_TAG} -m "${TRAVIS_TAG}" && \ - git push --quiet origin ${TRAVIS_TAG} - fi - -cache: - directories: - - $HOME/.composer/cache - - tmp 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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 162e250c6d..7ab0db920a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,74 +1,134 @@ -# Contributor Code of Conduct + +# Contributor Covenant Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project maintainer at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +ondrej@mirtes.cz. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/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 new file mode 100644 index 0000000000..9e007ae2cc --- /dev/null +++ b/Makefile @@ -0,0 +1,140 @@ +.PHONY: tests + +build: cs tests phpstan + +tests: + XDEBUG_MODE=off php vendor/bin/paratest --runner WrapperRunner --no-coverage + +tests-integration: + php vendor/bin/paratest --runner WrapperRunner --no-coverage --group exec + +tests-levels: + php vendor/bin/paratest --runner WrapperRunner --no-coverage --group levels + +tests-coverage: + php vendor/bin/paratest --runner WrapperRunner + +tests-golden-reflection: + php vendor/bin/paratest --runner WrapperRunner --no-coverage tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + +lint: + 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 \ + --exclude tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php \ + --exclude tests/PHPStan/Rules/Classes/data/enum-sanity.php \ + --exclude tests/PHPStan/Rules/Classes/data/extends-error.php \ + --exclude tests/PHPStan/Rules/Classes/data/implements-error.php \ + --exclude tests/PHPStan/Rules/Classes/data/interface-extends-error.php \ + --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 \ + --exclude tests/PHPStan/Rules/Classes/data/duplicate-promoted-property.php \ + --exclude tests/PHPStan/Rules/Properties/data/default-value-for-promoted-property.php \ + --exclude tests/PHPStan/Rules/Operators/data/invalid-assign-var.php \ + --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 && XDEBUG_MODE=off php build-cs/vendor/bin/phpcs + +cs-fix: + XDEBUG_MODE=off php build-cs/vendor/bin/phpcbf + +phpstan: + php bin/phpstan clear-result-cache -q && php -d memory_limit=448M bin/phpstan + +phpstan-result-cache: + php -d memory_limit=448M bin/phpstan + +phpstan-generate-baseline: + php -d memory_limit=448M bin/phpstan --generate-baseline + +phpstan-generate-baseline-php: + php -d memory_limit=448M bin/phpstan analyse --generate-baseline phpstan-baseline.php + +phpstan-pro: + php -d memory_limit=448M bin/phpstan --pro + +name-collision: + php vendor/bin/detect-collisions --configuration build/collision-detector.json + +composer-dependency-analyser: + php vendor/bin/composer-dependency-analyser --config build/composer-dependency-analyser.php diff --git a/README.md b/README.md index 492c01788e..e817604542 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # PHPStan - PHP Static Analysis Tool -[![Build Status](https://travis-ci.com/phpstan/phpstan-src.svg?branch=master)](https://travis-ci.com/phpstan/phpstan-src) -[![Build](https://github.com/phpstan/phpstan-src/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-src/actions) +[![Build](https://github.com/phpstan/phpstan-src/workflows/Tests/badge.svg)](https://github.com/phpstan/phpstan-src/actions) [![PHPStan Enabled](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan) --- @@ -12,16 +11,24 @@ 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 7.4. For distribution in `phpstan/phpstan` package and as a PHAR file, the source code is transformed to run on PHP 7.1 and higher. +PHPStan's source code is developed on PHP 8.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`, or `composer update` in case you aren't working in a directory which was built before. +Initially you need to run `composer install` in case you aren't working in a directory which was built before. Afterwards you can either run the whole build including linting and coding standards using ```bash -vendor/bin/phing +make ``` ### Running development version @@ -29,7 +36,7 @@ vendor/bin/phing You can also choose to run only part of the build. To analyse PHPStan by PHPStan itself, run: ```bash -vendor/bin/phing phpstan +make phpstan ``` ### Fixing code style @@ -37,27 +44,25 @@ vendor/bin/phing phpstan To detect code style issues, run: ```bash -vendor/bin/phing cs +make cs ``` -This requires PHP 7.4. On older versions the build target will be skipped and succeed silently. - And then to fix code style, run: ```bash -vendor/bin/phing cs-fix +make cs-fix ``` ### Running tests Run: ```bash -vendor/bin/phing tests +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 new file mode 100644 index 0000000000..f4c9cd19fc --- /dev/null +++ b/bin/functionMetadata_original.php @@ -0,0 +1,182 @@ + ['hasSideEffects' => false], + 'acos' => ['hasSideEffects' => false], + 'acosh' => ['hasSideEffects' => false], + 'addcslashes' => ['hasSideEffects' => false], + 'addslashes' => ['hasSideEffects' => false], + 'array_change_key_case' => ['hasSideEffects' => false], + 'array_chunk' => ['hasSideEffects' => false], + 'array_column' => ['hasSideEffects' => false], + 'array_combine' => ['hasSideEffects' => false], + 'array_count_values' => ['hasSideEffects' => false], + 'array_diff' => ['hasSideEffects' => false], + 'array_diff_assoc' => ['hasSideEffects' => false], + 'array_diff_key' => ['hasSideEffects' => false], + 'array_diff_uassoc' => ['hasSideEffects' => false], + 'array_diff_ukey' => ['hasSideEffects' => false], + 'array_fill' => ['hasSideEffects' => false], + 'array_fill_keys' => ['hasSideEffects' => false], + 'array_flip' => ['hasSideEffects' => false], + 'array_intersect' => ['hasSideEffects' => false], + 'array_intersect_assoc' => ['hasSideEffects' => false], + 'array_intersect_key' => ['hasSideEffects' => false], + 'array_intersect_uassoc' => ['hasSideEffects' => false], + 'array_intersect_ukey' => ['hasSideEffects' => false], + 'array_key_first' => ['hasSideEffects' => false], + 'array_key_last' => ['hasSideEffects' => false], + 'array_key_exists' => ['hasSideEffects' => false], + 'array_keys' => ['hasSideEffects' => false], + '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], + 'array_udiff_assoc' => ['hasSideEffects' => false], + 'array_udiff_uassoc' => ['hasSideEffects' => false], + 'array_uintersect' => ['hasSideEffects' => false], + '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], + 'atan' => ['hasSideEffects' => false], + 'atan2' => ['hasSideEffects' => false], + 'atanh' => ['hasSideEffects' => false], + 'base64_decode' => ['hasSideEffects' => false], + 'base64_encode' => ['hasSideEffects' => false], + 'base_convert' => ['hasSideEffects' => false], + 'basename' => ['hasSideEffects' => false], + 'bcadd' => ['hasSideEffects' => false], + 'bccomp' => ['hasSideEffects' => false], + 'bcdiv' => ['hasSideEffects' => false], + 'bcmod' => ['hasSideEffects' => false], + 'bcmul' => ['hasSideEffects' => false], + // continue functionMap.php, line 424 + 'chgrp' => ['hasSideEffects' => true], + 'chmod' => ['hasSideEffects' => true], + 'chown' => ['hasSideEffects' => true], + 'copy' => ['hasSideEffects' => true], + 'count' => ['hasSideEffects' => false], + 'error_log' => ['hasSideEffects' => true], + 'fclose' => ['hasSideEffects' => true], + 'fflush' => ['hasSideEffects' => true], + 'fgetc' => ['hasSideEffects' => true], + 'fgetcsv' => ['hasSideEffects' => true], + 'fgets' => ['hasSideEffects' => true], + 'fgetss' => ['hasSideEffects' => true], + 'file_put_contents' => ['hasSideEffects' => true], + 'flock' => ['hasSideEffects' => true], + 'fopen' => ['hasSideEffects' => true], + 'fpassthru' => ['hasSideEffects' => true], + 'fputcsv' => ['hasSideEffects' => true], + 'fputs' => ['hasSideEffects' => true], + 'fread' => ['hasSideEffects' => true], + 'fscanf' => ['hasSideEffects' => true], + 'fseek' => ['hasSideEffects' => true], + 'ftruncate' => ['hasSideEffects' => true], + 'fwrite' => ['hasSideEffects' => true], + '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], + 'rename' => ['hasSideEffects' => true], + '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], + 'touch' => ['hasSideEffects' => true], + 'umask' => ['hasSideEffects' => true], + 'unlink' => ['hasSideEffects' => true], + + // random functions, do not have side effects but are not deterministic + 'mt_rand' => ['hasSideEffects' => true], + 'rand' => ['hasSideEffects' => true], + 'random_bytes' => ['hasSideEffects' => true], + 'random_int' => ['hasSideEffects' => true], + + // methods + 'DateTime::createFromFormat' => ['hasSideEffects' => false], + 'DateTime::createFromImmutable' => ['hasSideEffects' => false], + 'DateTime::getLastErrors' => ['hasSideEffects' => false], + 'DateTime::add' => ['hasSideEffects' => true], + 'DateTime::modify' => ['hasSideEffects' => true], + 'DateTime::setDate' => ['hasSideEffects' => true], + 'DateTime::setISODate' => ['hasSideEffects' => true], + 'DateTime::setTime' => ['hasSideEffects' => true], + 'DateTime::setTimestamp' => ['hasSideEffects' => true], + 'DateTime::setTimezone' => ['hasSideEffects' => true], + 'DateTime::sub' => ['hasSideEffects' => true], + 'DateTime::diff' => ['hasSideEffects' => false], + 'DateTime::format' => ['hasSideEffects' => false], + 'DateTime::getOffset' => ['hasSideEffects' => false], + 'DateTime::getTimestamp' => ['hasSideEffects' => false], + 'DateTime::getTimezone' => ['hasSideEffects' => false], + + 'DateTimeImmutable::createFromFormat' => ['hasSideEffects' => false], + 'DateTimeImmutable::createFromMutable' => ['hasSideEffects' => false], + 'DateTimeImmutable::getLastErrors' => ['hasSideEffects' => false], + 'DateTimeImmutable::add' => ['hasSideEffects' => false], + 'DateTimeImmutable::modify' => ['hasSideEffects' => false], + 'DateTimeImmutable::setDate' => ['hasSideEffects' => false], + 'DateTimeImmutable::setISODate' => ['hasSideEffects' => false], + 'DateTimeImmutable::setTime' => ['hasSideEffects' => false], + 'DateTimeImmutable::setTimestamp' => ['hasSideEffects' => false], + 'DateTimeImmutable::setTimezone' => ['hasSideEffects' => false], + 'DateTimeImmutable::sub' => ['hasSideEffects' => false], + 'DateTimeImmutable::diff' => ['hasSideEffects' => false], + 'DateTimeImmutable::format' => ['hasSideEffects' => false], + '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 new file mode 100755 index 0000000000..d161d374e4 --- /dev/null +++ b/bin/generate-function-metadata.php @@ -0,0 +1,197 @@ +#!/usr/bin/env php +createForNewestSupportedVersion(); + $finder = new Finder(); + $finder->in(__DIR__ . '/../vendor/jetbrains/phpstorm-stubs')->files()->name('*.php'); + + $visitor = new class() extends NodeVisitorAbstract { + + /** @var string[] */ + public array $functions = []; + + /** @var list */ + public array $impureFunctions = []; + + /** @var string[] */ + 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() !== 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; + } + } + } + + if ($node instanceof Node\Stmt\ClassMethod) { + $class = $node->getAttribute('parent'); + if (!$class instanceof Node\Stmt\ClassLike) { + throw new ShouldNotHappenException($node->name->toString()); + } + $className = $class->namespacedName->toString(); + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() === Pure::class) { + $this->methods[] = sprintf('%s::%s', $className, $node->name->toString()); + break 2; + } + } + } + } + + return null; + } + + }; + + foreach ($finder as $stubFile) { + $path = $stubFile->getPathname(); + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver()); + $traverser->addVisitor(new NodeConnectingVisitor()); + $traverser->addVisitor($visitor); + + $traverser->traverse( + $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']) { + 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 ShouldNotHappenException($methodName); + } + } + $metadata[$methodName] = ['hasSideEffects' => false]; + } + + ksort($metadata); + + $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 +]; +php; + $content = ''; + foreach ($metadata as $name => $meta) { + $content .= sprintf( + "\t%s => [%s => %s],\n", + var_export($name, true), + var_export('hasSideEffects', true), + var_export($meta['hasSideEffects'], true), + ); + } + + 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 f52278f560..119af4377c 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -3,24 +3,41 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\ClearResultCacheCommand; -use PHPStan\Command\DumpDependenciesCommand; +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'); - 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']; if ( !array_key_exists('e88992873b7765f9b5710cab95ba5dd7', $composerAutoloadFiles) || !array_key_exists('3e76f7f02b41af8cea96018933f6b7e3', $composerAutoloadFiles) + || !array_key_exists('a4a119a56e50fbb293281d9a48007e0e', $composerAutoloadFiles) + || !array_key_exists('0e6d7bf4a5811bfa5cf40c5ccd6fae6a', $composerAutoloadFiles) + || !array_key_exists('e69f7f6ee287b969198c3c9d6777bd38', $composerAutoloadFiles) + || !array_key_exists('8825ede83f2f289127722d4e842cf7e8', $composerAutoloadFiles) + || !array_key_exists('23c18046f52bef3eea034657bafda50f', $composerAutoloadFiles) ) { echo "Composer autoloader changed\n"; exit(1); @@ -31,63 +48,110 @@ use PHPStan\Command\WorkerCommand; // fix unprefixed Hoa namespace - files already loaded 'e88992873b7765f9b5710cab95ba5dd7' => true, '3e76f7f02b41af8cea96018933f6b7e3' => true, + + // vendor/symfony/polyfill-php80/bootstrap.php + 'a4a119a56e50fbb293281d9a48007e0e' => true, + + // vendor/symfony/polyfill-mbstring/bootstrap.php + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => true, + + // vendor/symfony/polyfill-intl-normalizer/bootstrap.php + 'e69f7f6ee287b969198c3c9d6777bd38' => true, + + // vendor/symfony/polyfill-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(); - } 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%'); + + $composerAutoloaderProjectPaths = array_map(function(string $s): string { + return str_replace(DIRECTORY_SEPARATOR, '/', $s); + }, $composerAutoloaderProjectPaths); + $reversedComposerAutoloaderProjectPaths = array_values(array_unique(array_reverse($composerAutoloaderProjectPaths))); - $reversedComposerAutoloaderProjectPaths = array_reverse($composerAutoloaderProjectPaths); - $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths)); - $application->add(new DumpDependenciesCommand($reversedComposerAutoloaderProjectPaths)); + $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/bin/transform-source.php b/bin/transform-source.php deleted file mode 100755 index 8ecbefb023..0000000000 --- a/bin/transform-source.php +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env php -type === null) { - return null; - } - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $node->type = null; - return $node; - } - - $node->setDocComment(new \PhpParser\Comment\Doc(sprintf('/** @var %s */', $this->printType($node->type)))); - $node->type = null; - - return $node; - } - - /** - * @param Identifier|Name|NullableType|UnionType $type - * @return string - */ - private function printType($type): string - { - if ($type instanceof NullableType) { - return $this->printType($type->type) . '|null'; - } - - if ($type instanceof UnionType) { - throw new \Exception('UnionType not yet supported'); - } - - if ($type instanceof Name) { - $name = $type->toString(); - if ($type->isFullyQualified()) { - return '\\' . $name; - } - - return $name; - } - - if ($type instanceof Identifier) { - return $type->name; - } - - throw new \Exception('Unsupported type class'); - } - -} - -(function () { - $dir = __DIR__ . '/../src'; - - $lexer = new Lexer\Emulative([ - 'usedAttributes' => [ - 'comments', - 'startLine', 'endLine', - 'startTokenPos', 'endTokenPos', - ], - ]); - $parser = new Parser\Php7($lexer, [ - 'useIdentifierNodes' => true, - 'useConsistentVariableNodes' => true, - 'useExpressionStatements' => true, - 'useNopStatements' => false, - ]); - $nameResolver = new NodeVisitor\NameResolver(null, [ - 'replaceNodes' => false - ]); - - $printer = new PrettyPrinter\Standard(); - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NodeVisitor\CloningVisitor()); - $traverser->addVisitor($nameResolver); - $traverser->addVisitor(new PhpPatcher($printer)); - - $it = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir), - RecursiveIteratorIterator::LEAVES_ONLY - ); - foreach ($it as $file) { - $fileName = $file->getPathname(); - if (!preg_match('/\.php$/', $fileName)) { - continue; - } - - $code = \PHPStan\File\FileReader::read($fileName); - $origStmts = $parser->parse($code); - $newCode = $printer->printFormatPreserving( - $traverser->traverse($origStmts), - $origStmts, - $lexer->getTokens() - ); - - \PHPStan\File\FileWriter::write($fileName, $newCode); - } -})(); diff --git a/build-cs/.gitignore b/build-cs/.gitignore index ff72e2d08c..61ead86667 100644 --- a/build-cs/.gitignore +++ b/build-cs/.gitignore @@ -1,2 +1 @@ -/composer.lock /vendor diff --git a/build-cs/composer.json b/build-cs/composer.json index c2128290d3..16a240bc97 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,8 +1,13 @@ { "require-dev": { - "consistence/coding-standard": "^3.10", - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2", - "slevomat/coding-standard": "^6.3.0", + "consistence-community/coding-standard": "^3.11.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "slevomat/coding-standard": "^8.8.0", "squizlabs/php_codesniffer": "^3.5.3" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/build-cs/composer.lock b/build-cs/composer.lock new file mode 100644 index 0000000000..e0bfbd4cae --- /dev/null +++ b/build-cs/composer.lock @@ -0,0 +1,333 @@ +{ + "_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": "e69c1916405a7e3c8001c1b609a0ee61", + "packages": [], + "packages-dev": [ + { + "name": "consistence-community/coding-standard", + "version": "3.11.3", + "source": { + "type": "git", + "url": "https://github.com/consistence-community/coding-standard.git", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "shasum": "" + }, + "require": { + "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.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpunit/phpunit": "9.5.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Consistence\\": [ + "Consistence" + ] + }, + "classmap": [ + "Consistence" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Vašek Purchart", + "email": "me@vasekpurchart.cz", + "homepage": "http://vasekpurchart.cz/" + } + ], + "description": "Consistence - Coding Standard - PHP Code Sniffer rules", + "keywords": [ + "Coding Standard", + "PHPCodeSniffer", + "codesniffer", + "coding", + "cs", + "phpcs", + "ruleset", + "sniffer", + "standard" + ], + "support": { + "issues": "https://github.com/consistence-community/coding-standard/issues", + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" + }, + "time": "2023-03-27T14:55:41+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "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", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl/", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com/", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "time": "2023-01-05T11:28:13+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.24.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "bcad8d995980440892759db0c32acae7c8e79442" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", + "reference": "bcad8d995980440892759db0c32acae7c8e79442", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "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.24.2" + }, + "time": "2023-09-26T12:28:12+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.14.1", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "shasum": "" + }, + "require": { + "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.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": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2023-10-08T07:28:08+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "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": "2023-02-22T23:07:41+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/build.xml b/build.xml deleted file mode 100644 index 3fd9b25d4a..0000000000 --- a/build.xml +++ /dev/null @@ -1,527 +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 8ad0e108c1..22812240f4 100644 --- a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php +++ b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php @@ -6,13 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ServiceLocatorDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +final class ServiceLocatorDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension { public function getClass(): string @@ -30,23 +27,24 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (count($methodCall->args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - $argType = $scope->getType($methodCall->args[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (count($methodCall->getArgs()) === 0) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); } - $type = new ObjectType($argType->getValue()); - if ($methodReflection->getName() === 'getByType' && count($methodCall->args) >= 2) { - $argType = $scope->getType($methodCall->args[1]->value); - if ($argType instanceof ConstantBooleanType && $argType->getValue()) { - $type = TypeCombinator::addNull($type); + $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + + 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); + } } } - return $type; + return $returnType; } } diff --git a/build/baseline-32bit.neon b/build/baseline-32bit.neon new file mode 100644 index 0000000000..82adbae209 --- /dev/null +++ b/build/baseline-32bit.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$value of class PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType constructor expects int, float given\\.$#" + count: 2 + path: ../src/Analyser/MutatingScope.php diff --git a/build/baseline-7.3.neon b/build/baseline-7.3.neon new file mode 100644 index 0000000000..0e3557e728 --- /dev/null +++ b/build/baseline-7.3.neon @@ -0,0 +1,14 @@ +parameters: + ignoreErrors: + - + message: + """ + #^Call to deprecated method assertFileNotExists\\(\\) of class PHPUnit\\\\Framework\\\\Assert\\: + https\\://github\\.com/sebastianbergmann/phpunit/issues/4077$# + """ + count: 1 + path: ../src/Testing/LevelsTestCase.php + - + message: "#^Call to function method_exists\\(\\) with 'PHPUnit\\\\\\\\Framework\\\\\\\\TestCase' and 'assertFileDoesNotEx…' will always evaluate to true\\.$#" + count: 1 + path: ../src/Testing/LevelsTestCase.php diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon new file mode 100644 index 0000000000..82e6b89e0c --- /dev/null +++ b/build/baseline-7.4.neon @@ -0,0 +1,83 @@ +parameters: + ignoreErrors: + - + 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\\\\Parallel\\\\ParallelAnalyser has an uninitialized property \\$processPool\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Parallel/ParallelAnalyser.php + + - + 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\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodes\\. 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 \\$phpDocNode\\. 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 \\$phpDocString\\. 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 \\$filename\\. 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 \\$templateTypeMap\\. 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 \\$templateTags\\. 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 \\$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 + path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + + - + message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$contents\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + + - + message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$classNodes\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + + - + message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$functionNodes\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + + - + message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$constantNodes\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + - + message: "#^Class PHPStan\\\\Reflection\\\\ReflectionProvider\\\\SetterReflectionProviderProvider has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Reflection/ReflectionProvider/SetterReflectionProviderProvider.php diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon new file mode 100644 index 0000000000..95dfa6cf8a --- /dev/null +++ b/build/baseline-8.0.neon @@ -0,0 +1,36 @@ +parameters: + ignoreErrors: + - + 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 list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between int<0, max> and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php + + - + message: "#^Call to function is_bool\\(\\) with string will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/SubstrDynamicReturnTypeExtension.php diff --git a/build/baseline-8.1.neon b/build/baseline-8.1.neon new file mode 100644 index 0000000000..aab4991158 --- /dev/null +++ b/build/baseline-8.1.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] 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 0cd3079214..0000000000 --- a/build/composer-require-checker.json +++ /dev/null @@ -1,26 +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", - "JSON_THROW_ON_ERROR", "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" - ], - "php-core-extensions" : [ - "Core", - "date", - "pcre", - "Phar", - "Reflection", - "SPL", - "standard", - "pcntl", - "mbstring", - "hash", - "tokenizer" - ] -} 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/enums.neon b/build/enums.neon new file mode 100644 index 0000000000..44eaccbbd1 --- /dev/null +++ b/build/enums.neon @@ -0,0 +1,19 @@ +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-architecture.neon.php b/build/ignore-by-architecture.neon.php new file mode 100644 index 0000000000..a6c86b46cf --- /dev/null +++ b/build/ignore-by-architecture.neon.php @@ -0,0 +1,11 @@ +load(__DIR__ . '/baseline-32bit.neon'); +} + +return []; diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php new file mode 100644 index 0000000000..c250ea9eec --- /dev/null +++ b/build/ignore-by-php-version.neon.php @@ -0,0 +1,42 @@ += 80000) { + $includes[] = __DIR__ . '/baseline-8.0.neon'; +} +if (PHP_VERSION_ID >= 80100) { + $includes[] = __DIR__ . '/baseline-8.1.neon'; +} else { + $includes[] = __DIR__ . '/enums.neon'; + $includes[] = __DIR__ . '/readonly-property.neon'; +} + +if (PHP_VERSION_ID >= 70400) { + $includes[] = __DIR__ . '/ignore-gte-php7.4-errors.neon'; +} + +if (PHP_VERSION_ID < 80000) { + $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 3dae92de53..d5ae8bada3 100644 --- a/build/ignore-gte-php7.4-errors.neon +++ b/build/ignore-gte-php7.4-errors.neon @@ -1,5 +1,10 @@ +includes: + - baseline-7.4.neon + parameters: ignoreErrors: + - '#^Class PHPStan\\Rules\\RuleErrors\\RuleError(?:\d+) has an uninitialized property (?:\$message|\$line|\$identifier|\$tip|\$file|\$metadata)#' + - '#Extension has an uninitialized property (?:\$typeSpecifier|\$broker)#' - - message: "#^Call to function method_exists\\(\\) with ReflectionProperty and '(?:hasType|getType)' will always evaluate to true\\.$#" - path: ../src/Reflection/Php/PhpClassReflectionExtension.php + message: '#has an uninitialized property#' + path: ../tests diff --git a/build/ignore-hoa.neon b/build/ignore-hoa.neon deleted file mode 100644 index 6accc42618..0000000000 --- a/build/ignore-hoa.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - excludes_analyse: - - ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php diff --git a/build/ignore-windows-runtime.neon b/build/ignore-windows-runtime.neon deleted file mode 100644 index 69479d766a..0000000000 --- a/build/ignore-windows-runtime.neon +++ /dev/null @@ -1,5 +0,0 @@ -parameters: - ignoreErrors: - - '#^Function pcntl_async_signals not found\.$#' - - '#^Function pcntl_signal not found\.$#' - - '#^Constant SIGINT not found\.$#' diff --git a/build/key.gpg.enc b/build/key.gpg.enc deleted file mode 100644 index 754af04785..0000000000 Binary files a/build/key.gpg.enc and /dev/null differ 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 90fc22eb59..b285f56e12 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -1,22 +1,72 @@ includes: - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon - - ../vendor/phpstan/phpstan-php-parser/extension.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 - ../phpstan-baseline.neon + - ../phpstan-baseline.php + - ignore-by-php-version.neon.php + - ignore-by-architecture.neon.php + parameters: + level: 8 + paths: + - PHPStan + - ../src + - ../tests bootstrapFiles: - - %rootDir%/tests/phpstan-bootstrap.php - excludes_analyse: - - %rootDir%/src/Reflection/SignatureMap/functionMap.php - - %rootDir%/src/Reflection/SignatureMap/functionMetadata.php - - %rootDir%/tests/*/data/* - - %rootDir%/tests/tmp/* - - %rootDir%/tests/PHPStan/Analyser/traits/* - - %rootDir%/tests/notAutoloaded/* - - %rootDir%/tests/PHPStan/Generics/functions.php + - ../tests/phpstan-bootstrap.php + cache: + nodesByStringCountMax: 128 + checkUninitializedProperties: true + checkMissingCallableSignature: true + excludePaths: + - ../tests/*/data/* + - ../tests/tmp/* + - ../tests/PHPStan/Analyser/nsrt/* + - ../tests/PHPStan/Analyser/traits/* + - ../tests/notAutoloaded/* + - ../tests/PHPStan/Reflection/UnionTypesTest.php + - ../tests/PHPStan/Reflection/MixedTypeTest.php + - ../tests/e2e/magic-setter/* + - ../tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php + - ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php + - ../src/Command/IgnoredRegexValidator.php + exceptions: + uncheckedExceptionClasses: + - 'PHPStan\ShouldNotHappenException' + - 'Symfony\Component\Console\Exception\InvalidArgumentException' + - 'PHPStan\BetterReflection\SourceLocator\Exception\InvalidFileLocation' + - 'PHPStan\BetterReflection\SourceLocator\Exception\InvalidArgumentException' + - 'Symfony\Component\Finder\Exception\DirectoryNotFoundException' + - 'InvalidArgumentException' + - 'PHPStan\DependencyInjection\ParameterNotFoundException' + - 'PHPStan\DependencyInjection\DuplicateIncludedFilesException' + - 'PHPStan\Analyser\UndefinedVariableException' + - 'RuntimeException' + - 'Nette\Neon\Exception' + - 'Nette\Utils\JsonException' + - 'PHPStan\File\CouldNotReadFileException' + - 'PHPStan\File\CouldNotWriteFileException' + - 'PHPStan\Parser\ParserErrorsException' + - 'ReflectionException' + - 'Nette\Utils\AssertionException' + - 'PHPStan\File\PathNotFoundException' + - 'PHPStan\Broker\ClassNotFoundException' + - 'PHPStan\Broker\FunctionNotFoundException' + - 'PHPStan\Broker\ConstantNotFoundException' + - 'PHPStan\Reflection\MissingMethodFromReflectionException' + - 'PHPStan\Reflection\MissingPropertyFromReflectionException' + - 'PHPStan\Reflection\MissingConstantFromReflectionException' + - 'PHPStan\Type\CircularTypeAliasDefinitionException' + - 'PHPStan\Broker\ClassAutoloadingException' + - 'LogicException' + - 'Error' + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true ignoreErrors: - '#^Dynamic call to static method PHPUnit\\Framework\\\S+\(\)\.$#' - '#should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule::processNode\(\)$#' @@ -29,40 +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.php - - stubs/ReactStreams.php + - 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 + class: PHPStan\Build\ContainerDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - - - class: PHPStan\Internal\UnionTypeGetInternalDynamicReturnTypeExtension - 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/phpstan.runtime-reflection.neon b/build/phpstan.runtime-reflection.neon deleted file mode 100644 index 59a74ec84b..0000000000 --- a/build/phpstan.runtime-reflection.neon +++ /dev/null @@ -1,8 +0,0 @@ -includes: - - phpstan.neon - -services: - reflectionProvider: - factory: @innerRuntimeReflectionProvider - autowired: - - PHPStan\Reflection\ReflectionProvider 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/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/NetteDIContainer.stub b/build/stubs/NetteDIContainer.stub new file mode 100644 index 0000000000..455eb43455 --- /dev/null +++ b/build/stubs/NetteDIContainer.stub @@ -0,0 +1,15 @@ + $type + * @return T + */ + public function getByType(string $type); + +} diff --git a/build/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/stubs/ReactChildProcess.php b/build/stubs/ReactChildProcess.stub similarity index 100% rename from build/stubs/ReactChildProcess.php rename to build/stubs/ReactChildProcess.stub diff --git a/build/stubs/ReactStreams.php b/build/stubs/ReactStreams.stub similarity index 100% rename from build/stubs/ReactStreams.php rename to build/stubs/ReactStreams.stub 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/codecov.yml b/codecov.yml deleted file mode 100644 index 642803cbcd..0000000000 --- a/codecov.yml +++ /dev/null @@ -1,4 +0,0 @@ -comment: false -coverage: - status: - patch: off 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.json b/compiler/build/box.json index 36e11f02a7..9f0b7ee6c1 100644 --- a/compiler/build/box.json +++ b/compiler/build/box.json @@ -7,11 +7,13 @@ "KevinGH\\Box\\Compactor\\PhpScoper" ], "files": [ - "preload.php" + "preload.php", + "vendor/composer/installed.php" ], "directories": [ "conf", "src", + "resources", "stubs" ], "force-autodiscovery": true, diff --git a/compiler/build/box.phar b/compiler/build/box.phar old mode 100644 new mode 100755 index 70e1805692..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 fdd07820e9..0d0008d341 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -3,15 +3,24 @@ require_once __DIR__ . '/../vendor/autoload.php'; $stubs = [ - '../../src/Reflection/SignatureMap/functionMap.php', - '../../src/Reflection/SignatureMap/functionMap_php74delta.php', - '../../src/Reflection/SignatureMap/functionMetadata.php', + '../../resources/functionMap.php', + '../../resources/functionMap_php74delta.php', + '../../resources/functionMap_php80delta.php', + '../../resources/functionMetadata.php', '../../vendor/hoa/consistency/Prelude.php', + '../../vendor/composer/InstalledVersions.php', + '../../vendor/composer/installed.php', ]; $stubFinder = \Isolated\Symfony\Component\Finder\Finder::create(); foreach ($stubFinder->files()->name('*.php')->in([ '../../stubs', '../../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-intl-grapheme', ]) as $file) { if ($file->getPathName() === '../../vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php') { continue; @@ -19,10 +28,21 @@ $stubs[] = $file->getPathName(); } +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' => null, + 'prefix' => $prefix, 'finders' => [], - 'files-whitelist' => $stubs, + 'exclude-files' => $stubs, 'patchers' => [ function (string $filePath, string $prefix, string $content): string { if ($filePath !== 'bin/phpstan') { @@ -30,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; @@ -37,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( @@ -70,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/TestCase.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; @@ -150,14 +160,23 @@ 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/TestCase.php', + 'src/Testing/PHPStanTestCase.php', + 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/Type/ComposerSourceLocator.php', ], true)) { return $content; } 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; @@ -167,10 +186,81 @@ function (string $filePath, string $prefix, string $content): string { return $content; }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/phpstan/php-8-stubs/Php8StubsMap.php') { + return $content; + } + + $content = str_replace('\'' . $prefix . '\\\\', '\'', $content); + + return $content; + }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + return $content; + } + + return str_replace('Core/Core_d.php', 'Core/Core_d.stub', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + return $content; + } + + return str_replace(sprintf('\'%s\\\\JetBrains\\\\', $prefix), '\'JetBrains\\\\', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/nikic/php-parser/lib')) { + return $content; + } + + return str_replace(sprintf('use %s\\PhpParser;', $prefix), 'use PhpParser;', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/nikic/php-parser/lib')) { + return $content; + } + + return str_replace([ + sprintf('\\%s', $prefix), + sprintf('\\\\%s', $prefix), + ], '', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/ondrejmirtes/better-reflection')) { + return $content; + } + + return str_replace(sprintf('%s\\PropertyHookType', $prefix), 'PropertyHookType', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if ( + $filePath !== 'vendor/nette/utils/src/Utils/Strings.php' + && $filePath !== 'vendor/nette/utils/src/Utils/Arrays.php' + ) { + return $content; + } + + return str_replace('#[\\JetBrains\\PhpStorm\\Language(\'RegExp\')] ', '', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/fidry/cpu-core-counter/src/Finder/WindowsRegistryLogicalFinder.php') { + return $content; + } + return str_replace(sprintf('%s\\\\reg query', $prefix), 'reg query', $content); + }, ], - 'whitelist' => [ - 'PHPStan\*', - 'PhpParser\*', - 'Hoa\*', + 'exclude-namespaces' => [ + 'PHPStan', + 'PHPUnit', + 'PhpParser', + 'Hoa', + 'Symfony\Polyfill\Php80', + 'Symfony\Polyfill\Php81', + 'Symfony\Polyfill\Mbstring', + 'Symfony\Polyfill\Intl\Normalizer', + 'Symfony\Polyfill\Intl\Grapheme', ], + 'expose-global-functions' => false, + 'expose-global-classes' => false, ]; diff --git a/compiler/composer.json b/compiler/composer.json index 8c1a449eb6..4da1b08076 100644 --- a/compiler/composer.json +++ b/compiler/composer.json @@ -4,12 +4,13 @@ "description": "PHAR Compiler for PHPStan", "license": ["MIT"], "require": { - "php": "^7.1", + "php": "^8.0", "nette/neon": "^3.0.0", - "symfony/console": "^4.1", - "symfony/process": "^4.1", - "symfony/filesystem": "^4.1", - "symfony/finder": "^5.0" + "seld/phar-utils": "^1.2", + "symfony/console": "^6.0.0", + "symfony/filesystem": "^6.0.0", + "symfony/finder": "^6.0.0", + "symfony/process": "^6.0.0" }, "autoload": { "psr-4": { @@ -22,7 +23,16 @@ } }, "require-dev": { - "phpunit/phpunit": "^8.4", - "phpstan/phpstan-phpunit": "^0.12.8" - } + "phpunit/phpunit": "^9.5.1", + "phpstan/phpstan-phpunit": "^1.0" + }, + "config": { + "platform": { + "php": "8.0.99" + }, + "platform-check": false, + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/compiler/composer.lock b/compiler/composer.lock new file mode 100644 index 0000000000..d970a7b6f7 --- /dev/null +++ b/compiler/composer.lock @@ -0,0 +1,2817 @@ +{ + "_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": "7caab5611acadb20806eaeca4e294e98", + "packages": [ + { + "name": "nette/neon", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "372d945c156ee7f35c953339fb164538339e6283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/372d945c156ee7f35c953339fb164538339e6283", + "reference": "372d945c156ee7f35c953339fb164538339e6283", + "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.0" + }, + "time": "2023-01-13T03:08:29+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": "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.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c3ebc83d031b71c39da318ca8b7a07ecc67507ed", + "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "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.0.19" + }, + "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-01T08:36:10+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3d49eec03fda1f0fc19b7349fbbe55ebc1004214", + "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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 basic utilities for the filesystem", + "homepage": "https://symfony.com/", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.0.19" + }, + "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-20T17:44:14+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/5cc9cac6586fc0c28cd173780ca696e419fefa11", + "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11", + "shasum": "" + }, + "require": { + "php": ">=8.0.2" + }, + "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.0.19" + }, + "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-20T17:44:14+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/process", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "2114fd60f26a296cc403a7939ab91478475a33d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/2114fd60f26a296cc403a7939ab91478475a33d4", + "reference": "2114fd60f26a296cc403a7939ab91478475a33d4", + "shasum": "" + }, + "require": { + "php": ">=8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com/", + "support": { + "source": "https://github.com/symfony/process/tree/v6.0.19" + }, + "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-01T08:36:10+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "reference": "d78d39c1599bd1188b8e26bb341da52c3c6d8a66", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "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.0.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-05-30T19:17:58+00:00" + }, + { + "name": "symfony/string", + "version": "v6.0.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d9e72497367c23e08bf94176d2be45b00a9d232a", + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "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.0.19" + }, + "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-01T08:36:10+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^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/1.5.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:15:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "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.17.1" + }, + "time": "2023-08-13T19:53:39+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "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.3" + }, + "time": "2021-07-20T11:28:43+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": "phpstan/phpstan", + "version": "1.10.15", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", + "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": "2023-05-09T15:28:01+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "1.3.13", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "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" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.13" + }, + "time": "2023-05-26T11:05:59+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" + }, + "dist": { + "type": "zip", + "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.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-07-26T13:44:30+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.11", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0", + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.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.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11" + }, + "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": "2023-08-19T07:10:56+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "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.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+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.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "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.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "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.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+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.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "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.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bde739e7565280bda77be70044ac1047bc007e34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "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.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-02T09:26:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "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.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+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.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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" + } + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+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.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "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.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.0" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.0.99" + }, + "plugin-api-version": "2.3.0" +} diff --git a/compiler/patches/Consistency.diff b/compiler/patches/Consistency.diff deleted file mode 100644 index 7b279857ed..0000000000 --- a/compiler/patches/Consistency.diff +++ /dev/null @@ -1,45 +0,0 @@ ---- ../vendor/hoa/consistency/Consistency.php 2017-05-02 14:18:12.000000000 +0200 -+++ ../vendor/hoa/consistency/Consistency2.php 2020-05-05 08:28:35.000000000 +0200 -@@ -319,42 +319,6 @@ - $define('STREAM_CRYPTO_METHOD_ANY_CLIENT', 63); - } - --if (!function_exists('curry')) { -- /** -- * Curry. -- * Example: -- * $c = curry('str_replace', …, …, 'foobar'); -- * var_dump($c('foo', 'baz')); // bazbar -- * $c = curry('str_replace', 'foo', 'baz', …); -- * var_dump($c('foobarbaz')); // bazbarbaz -- * Nested curries also work: -- * $c1 = curry('str_replace', …, …, 'foobar'); -- * $c2 = curry($c1, 'foo', …); -- * var_dump($c2('baz')); // bazbar -- * Obviously, as the first argument is a callable, we can combine this with -- * \Hoa\Consistency\Xcallable ;-). -- * The “…” character is the HORIZONTAL ELLIPSIS Unicode character (Unicode: -- * 2026, UTF-8: E2 80 A6). -- * -- * @param mixed $callable Callable (two parts). -- * @param ... ... Arguments. -- * @return \Closure -- */ -- function curry($callable) -- { -- $arguments = func_get_args(); -- array_shift($arguments); -- $ii = array_keys($arguments, …, true); -- -- return function () use ($callable, $arguments, $ii) { -- return call_user_func_array( -- $callable, -- array_replace($arguments, array_combine($ii, func_get_args())) -- ); -- }; -- } --} -- - /** - * Flex entity. - */ diff --git a/compiler/patches/Wrapper.diff b/compiler/patches/Wrapper.diff deleted file mode 100644 index b07c9b0b3b..0000000000 --- a/compiler/patches/Wrapper.diff +++ /dev/null @@ -1,27 +0,0 @@ ---- ../vendor/hoa/protocol/Wrapper.php 2017-01-14 13:26:10.000000000 +0100 -+++ ../vendor/hoa/protocol/Wrapper2.php 2020-05-05 08:39:18.000000000 +0200 -@@ -582,24 +582,3 @@ - stream_wrapper_register('hoa', Wrapper::class); - - } -- --namespace --{ -- --/** -- * Alias of `Hoa\Protocol::resolve` method. -- * -- * @param string $path Path to resolve. -- * @param bool $exists If `true`, try to find the first that exists, -- * else return the first solution. -- * @param bool $unfold Return all solutions instead of one. -- * @return mixed -- */ --if (!function_exists('resolve')) { -- function resolve($path, $exists = true, $unfold = false) -- { -- return Hoa\Protocol::getInstance()->resolve($path, $exists, $unfold); -- } --} -- --} diff --git a/compiler/patches/stubs/PDO/PDO.stub.patch b/compiler/patches/stubs/PDO/PDO.stub.patch deleted file mode 100644 index 089aa13d21..0000000000 --- a/compiler/patches/stubs/PDO/PDO.stub.patch +++ /dev/null @@ -1,43 +0,0 @@ ---- PDO.stub 2019-12-05 17:56:26.000000000 +0100 -+++ PDO2.stub 2020-05-26 14:16:32.000000000 +0200 -@@ -268,7 +268,6 @@ - * As PDO::FETCH_INTO but object is provided as a serialized string. - * Available since PHP 5.1.0. Since PHP 5.3.0 the class constructor is never called if this - * flag is set. -- * @since 5.1 Available - * @link https://php.net/manual/en/pdo.constants.php#pdo.constants.fetch-serialize - */ - const FETCH_SERIALIZE = 524288; -@@ -727,7 +726,7 @@ - *

- * Note, this constant can only be used in the driver_options array when constructing a new database handle. - *

-- * @since 5.5.21 and 5.6.5 -+ * @since 5.5.21 - * @link https://php.net/manual/en/ref.pdo-mysql.php#pdo.constants.mysql-attr-multi-statements - */ - const MYSQL_ATTR_MULTI_STATEMENTS = 1015; -@@ -747,6 +746,7 @@ - */ - const PGSQL_ASSOC = 1; - const PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT = 1000; -+ const PGSQL_ATTR_DISABLE_PREPARES = 1000; - const PGSQL_BAD_RESPONSE = 5; - const PGSQL_BOTH = 3; - const PGSQL_TRANSACTION_IDLE = 0; -@@ -839,6 +839,15 @@ - */ - const SQLITE_ATTR_EXTENDED_RESULT_CODES = 2; - -+ const FB_ATTR_DATE_FORMAT = 1; -+ const FB_ATTR_TIME_FORMAT = 2; -+ const FB_ATTR_TIMESTAMP_FORMAT = 3; -+ -+ const OCI_ATTR_ACTION = 1; -+ const OCI_ATTR_CLIENT_INFO = 2; -+ const OCI_ATTR_CLIENT_IDENTIFIER = 3; -+ const OCI_ATTR_MODULE = 4; -+ - /** - * (PHP 5 >= 5.1.0, PHP 7, PECL pdo >= 0.1.0)
- * Creates a PDO instance representing a connection to a database diff --git a/compiler/src/Console/CompileCommand.php b/compiler/src/Console/CompileCommand.php deleted file mode 100644 index b988135557..0000000000 --- a/compiler/src/Console/CompileCommand.php +++ /dev/null @@ -1,215 +0,0 @@ -filesystem = $filesystem; - $this->processFactory = $processFactory; - $this->dataDir = $dataDir; - $this->buildDir = $buildDir; - } - - protected function configure(): void - { - $this->setName('phpstan:compile') - ->setDescription('Compile PHAR'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->processFactory->setOutput($output); - - $this->buildPreloadScript(); - $this->deleteUnnecessaryVendorCode(); - $this->patchFile( - $output, - 'vendor/hoa/consistency/Consistency.php', - 'compiler/patches/Consistency.diff' - ); - $this->patchFile( - $output, - 'vendor/hoa/protocol/Wrapper.php', - 'compiler/patches/Wrapper.diff' - ); - $this->fixComposerJson($this->buildDir); - $this->renamePhpStormStubs(); - $this->patchPhpStormStubs($output); - $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 = \Symfony\Component\Finder\Finder::create(); - $stubsMapPath = $directory . '/PhpStormStubsMap.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 \PHPStan\ShouldNotHappenException(sprintf('Could not rename %s', $path)); - } - } - - $stubsMapContents = file_get_contents($stubsMapPath); - if ($stubsMapContents === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not read %s', $stubsMapPath)); - } - - $stubsMapContents = str_replace('.php\',', '.stub\',', $stubsMapContents); - - $putSuccess = file_put_contents($directory . '/PhpStormStubsMap.php', $stubsMapContents); - if ($putSuccess === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); - } - } - - private function patchPhpStormStubs(OutputInterface $output): void - { - $stubFinder = \Symfony\Component\Finder\Finder::create(); - $stubsDirectory = __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs'; - foreach ($stubFinder->files()->name('*.patch')->in(__DIR__ . '/../../patches/stubs') as $patchFile) { - $absolutePatchPath = $patchFile->getPathname(); - $patchPath = $patchFile->getRelativePathname(); - $stubPath = realpath($stubsDirectory . '/' . dirname($patchPath) . '/' . basename($patchPath, '.patch')); - if ($stubPath === false) { - $output->writeln(sprintf('Stub %s not found.', $stubPath)); - continue; - } - $this->patchFile($output, $stubPath, $absolutePatchPath); - } - } - - 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([ - $vendorDir . '/nikic/php-parser/lib/PhpParser', - $vendorDir . '/phpstan/phpdoc-parser/src', - ]) 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 patchFile(OutputInterface $output, string $originalFile, string $patchFile): void - { - exec(sprintf( - 'patch -d %s %s %s', - escapeshellarg($this->buildDir), - escapeshellarg($originalFile), - escapeshellarg($patchFile) - ), $outputLines, $exitCode); - if ($exitCode === 0) { - return; - } - - $output->writeln(sprintf('Patching failed: %s', implode("\n", $outputLines))); - } - - private function transformSource(): void - { - exec(escapeshellarg(__DIR__ . '/../../../bin/transform-source.php'), $outputLines, $exitCode); - if ($exitCode === 0) { - return; - } - - throw new \PHPStan\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/Filesystem/SymfonyFilesystem.php b/compiler/src/Filesystem/SymfonyFilesystem.php index 7946e92626..e74d01fd74 100644 --- a/compiler/src/Filesystem/SymfonyFilesystem.php +++ b/compiler/src/Filesystem/SymfonyFilesystem.php @@ -2,15 +2,15 @@ namespace PHPStan\Compiler\Filesystem; +use RuntimeException; +use function file_get_contents; +use function file_put_contents; + final class SymfonyFilesystem implements Filesystem { - /** @var \Symfony\Component\Filesystem\Filesystem */ - private $filesystem; - - public function __construct(\Symfony\Component\Filesystem\Filesystem $filesystem) + public function __construct(private \Symfony\Component\Filesystem\Filesystem $filesystem) { - $this->filesystem = $filesystem; } public function exists(string $dir): bool @@ -32,7 +32,7 @@ public function read(string $file): string { $content = file_get_contents($file); if ($content === false) { - throw new \RuntimeException(); + throw new RuntimeException(); } return $content; } diff --git a/compiler/src/Process/DefaultProcessFactory.php b/compiler/src/Process/DefaultProcessFactory.php deleted file mode 100644 index 3f4a09a6b1..0000000000 --- a/compiler/src/Process/DefaultProcessFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -output = new NullOutput(); - } - - /** - * @param string[] $command - * @param string $cwd - * @return \PHPStan\Compiler\Process\Process - */ - 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 f892601f73..0000000000 --- a/compiler/src/Process/ProcessFactory.php +++ /dev/null @@ -1,19 +0,0 @@ - */ - private $process; - - /** - * @param string[] $command - * @param string $cwd - * @param \Symfony\Component\Console\Output\OutputInterface $output - */ - 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 e40d86d696..0000000000 --- a/compiler/tests/Console/CompileCommandTest.php +++ /dev/null @@ -1,54 +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->expects(self::at(0))->method('setOutput'); - $processFactory->expects(self::at(1))->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/Filesystem/SymfonyFilesystemTest.php b/compiler/tests/Filesystem/SymfonyFilesystemTest.php index 0d21aff557..5a0f486441 100644 --- a/compiler/tests/Filesystem/SymfonyFilesystemTest.php +++ b/compiler/tests/Filesystem/SymfonyFilesystemTest.php @@ -3,13 +3,15 @@ namespace PHPStan\Compiler\Filesystem; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; +use function unlink; final class SymfonyFilesystemTest extends TestCase { public function testExists(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $inner->expects(self::once())->method('exists')->with('foo')->willReturn(true); self::assertTrue((new SymfonyFilesystem($inner))->exists('foo')); @@ -17,7 +19,7 @@ public function testExists(): void public function testRemove(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $inner->expects(self::once())->method('remove')->with('foo')->willReturn(true); (new SymfonyFilesystem($inner))->remove('foo'); @@ -25,7 +27,7 @@ public function testRemove(): void public function testMkdir(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $inner->expects(self::once())->method('mkdir')->with('foo')->willReturn(true); (new SymfonyFilesystem($inner))->mkdir('foo'); @@ -33,7 +35,7 @@ public function testMkdir(): void public function testRead(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $content = (new SymfonyFilesystem($inner))->read(__DIR__ . '/data/composer.json'); self::assertSame("{}\n", $content); @@ -41,7 +43,7 @@ public function testRead(): void public function testWrite(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); @unlink(__DIR__ . '/data/test.json'); (new SymfonyFilesystem($inner))->write(__DIR__ . '/data/test.json', "{}\n"); diff --git a/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 7050160940..3a585da23c 100644 --- a/composer.json +++ b/composer.json @@ -5,57 +5,122 @@ "MIT" ], "require": { - "php": "^7.4", + "php": "^8.1", + "composer-runtime-api": "^2.0", "clue/ndjson-react": "^1.0", - "composer/xdebug-handler": "^1.3.0", + "composer/ca-bundle": "^1.2", + "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": "2019.3", + "hoa/file": "1.17.07.11", + "jetbrains/phpstorm-stubs": "dev-master#b22fb017543bb7147e3bcc53f08fb13a48aff994", "nette/bootstrap": "^3.0", - "nette/di": "^3.0", - "nette/finder": "^2.5", - "nette/neon": "^3.0", - "nette/schema": "^1.0", - "nette/utils": "^3.1.1", - "nikic/php-parser": "^4.5.0", - "ondram/ci-detector": "^3.1", - "ondrejmirtes/better-reflection": "^4.3.9", - "phpdocumentor/type-resolver": "1.0.1", - "phpstan/phpdoc-parser": "^0.4.8", - "react/child-process": "^0.6.1", - "react/event-loop": "^1.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": "^5.4.0", + "ondram/ci-detector": "^3.4.0", + "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": "^3.2", "react/socket": "^1.3", "react/stream": "^1.1", - "symfony/console": "^4.3", - "symfony/finder": "^4.3", - "symfony/service-contracts": "1.1.8" + "symfony/console": "^5.4.3", + "symfony/finder": "^5.4.3", + "symfony/polyfill-intl-grapheme": "^1.23", + "symfony/polyfill-intl-normalizer": "^1.23", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-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": "^2.0", - "ergebnis/composer-normalize": "^2.0.2", - "nategood/httpful": "^0.2.20", - "phing/phing": "^2.16.0", + "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": "^0.12.3", - "phpstan/phpstan-php-parser": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.5.18" + "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": "7.4.6" + "php": "8.1.99" }, - "sort-packages": true + "platform-check": false, + "sort-packages": true, + "allow-plugins": { + "cweagans/composer-patches": true + } }, "extra": { - "branch-alias": { - "dev-master": "0.12-dev" + "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": { @@ -63,7 +128,8 @@ "PHPStan\\": [ "src/" ] - } + }, + "files": ["src/debugScope.php", "src/dumpType.php", "src/autoloadFunctions.php", "src/Testing/functions.php"] }, "autoload-dev": { "psr-4": { @@ -76,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 new file mode 100644 index 0000000000..b304bec2cb --- /dev/null +++ b/composer.lock @@ -0,0 +1,6465 @@ +{ + "_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": "a5aee6235dc8ddeac7b42ed53ce87902", + "packages": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "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": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be/" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" + }, + "funding": [ + { + "url": "https://packagist.com/", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-03-06T14:30:56+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be/" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.1" + }, + "funding": [ + { + "url": "https://packagist.com/", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-08-27T18:44:43+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "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.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "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": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "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": [ + { + "url": "https://packagist.com/", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "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", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com/", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "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": "2020-11-24T22:02:12+00:00" + }, + { + "name": "hoa/compiler", + "version": "3.17.08.08", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Compiler.git", + "reference": "aa09caf0bf28adae6654ca6ee415ee2f522672de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Compiler/zipball/aa09caf0bf28adae6654ca6ee415ee2f522672de", + "reference": "aa09caf0bf28adae6654ca6ee415ee2f522672de", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/iterator": "~2.0", + "hoa/math": "~1.0", + "hoa/protocol": "~1.0", + "hoa/regex": "~1.0", + "hoa/visitor": "~2.0" + }, + "require-dev": { + "hoa/json": "~2.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Compiler\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Compiler library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "algebraic", + "ast", + "compiler", + "context-free", + "coverage", + "exhaustive", + "grammar", + "isotropic", + "language", + "lexer", + "library", + "ll1", + "llk", + "parser", + "pp", + "random", + "regular", + "rule", + "sampler", + "syntax", + "token", + "trace", + "uniform" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Compiler", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Compiler/issues", + "source": "https://central.hoa-project.net/Resource/Library/Compiler" + }, + "abandoned": true, + "time": "2017-08-08T07:44:07+00:00" + }, + { + "name": "hoa/consistency", + "version": "1.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "shasum": "" + }, + "require": { + "hoa/exception": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "hoa/stream": "~1.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "Prelude.php" + ], + "psr-4": { + "Hoa\\Consistency\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Consistency library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autoloader", + "callable", + "consistency", + "entity", + "flex", + "keyword", + "library" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Consistency", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Consistency/issues", + "source": "https://central.hoa-project.net/Resource/Library/Consistency" + }, + "abandoned": true, + "time": "2017-05-02T12:18:12+00:00" + }, + { + "name": "hoa/event", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Event.git", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Event\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Event library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "event", + "library", + "listener", + "observer" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Event", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Event/issues", + "source": "https://central.hoa-project.net/Resource/Library/Event" + }, + "abandoned": true, + "time": "2017-01-13T15:30:50+00:00" + }, + { + "name": "hoa/exception", + "version": "1.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Exception.git", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Exception\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Exception library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "exception", + "library" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Exception", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Exception/issues", + "source": "https://central.hoa-project.net/Resource/Library/Exception" + }, + "abandoned": true, + "time": "2017-01-16T07:53:27+00:00" + }, + { + "name": "hoa/file", + "version": "1.17.07.11", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/File.git", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/stream": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\File\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\File library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Socket", + "directory", + "file", + "finder", + "library", + "link", + "temporary" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/File", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/File/issues", + "source": "https://central.hoa-project.net/Resource/Library/File" + }, + "abandoned": true, + "time": "2017-07-11T07:42:15+00:00" + }, + { + "name": "hoa/iterator", + "version": "2.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Iterator.git", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Iterator\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Iterator library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "iterator", + "library" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Iterator", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Iterator/issues", + "source": "https://central.hoa-project.net/Resource/Library/Iterator" + }, + "abandoned": true, + "time": "2017-01-10T10:34:47+00:00" + }, + { + "name": "hoa/math", + "version": "1.17.05.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Math.git", + "reference": "7150785d30f5d565704912116a462e9f5bc83a0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Math/zipball/7150785d30f5d565704912116a462e9f5bc83a0c", + "reference": "7150785d30f5d565704912116a462e9f5bc83a0c", + "shasum": "" + }, + "require": { + "hoa/compiler": "~3.0", + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/protocol": "~1.0", + "hoa/zformat": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Math\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Math library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "arrangement", + "combination", + "combinatorics", + "counting", + "library", + "math", + "permutation", + "sampler", + "set" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Math", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Math/issues", + "source": "https://central.hoa-project.net/Resource/Library/Math" + }, + "abandoned": true, + "time": "2017-05-16T08:02:17+00:00" + }, + { + "name": "hoa/protocol", + "version": "1.17.01.14", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Protocol.git", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "Wrapper.php" + ], + "psr-4": { + "Hoa\\Protocol\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Protocol library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "protocol", + "resource", + "stream", + "wrapper" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Protocol", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Protocol/issues", + "source": "https://central.hoa-project.net/Resource/Library/Protocol" + }, + "abandoned": true, + "time": "2017-01-14T12:26:10+00:00" + }, + { + "name": "hoa/regex", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Regex.git", + "reference": "7e263a61b6fb45c1d03d8e5ef77668518abd5bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Regex/zipball/7e263a61b6fb45c1d03d8e5ef77668518abd5bec", + "reference": "7e263a61b6fb45c1d03d8e5ef77668518abd5bec", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/math": "~1.0", + "hoa/protocol": "~1.0", + "hoa/ustring": "~4.0", + "hoa/visitor": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Regex\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Regex library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "compiler", + "library", + "regex" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Regex", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Regex/issues", + "source": "https://central.hoa-project.net/Resource/Library/Regex" + }, + "abandoned": true, + "time": "2017-01-13T16:10:24+00:00" + }, + { + "name": "hoa/stream", + "version": "1.17.02.21", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Stream.git", + "reference": "3293cfffca2de10525df51436adf88a559151d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", + "reference": "3293cfffca2de10525df51436adf88a559151d82", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/protocol": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Stream\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Stream library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Context", + "bucket", + "composite", + "filter", + "in", + "library", + "out", + "protocol", + "stream", + "wrapper" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Stream", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Stream/issues", + "source": "https://central.hoa-project.net/Resource/Library/Stream" + }, + "abandoned": true, + "time": "2017-02-21T16:01:06+00:00" + }, + { + "name": "hoa/ustring", + "version": "4.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ustring.git", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", + "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ustring\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ustring library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "search", + "string", + "unicode" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Ustring", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Ustring/issues", + "source": "https://central.hoa-project.net/Resource/Library/Ustring" + }, + "abandoned": true, + "time": "2017-01-16T07:08:25+00:00" + }, + { + "name": "hoa/visitor", + "version": "2.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Visitor.git", + "reference": "c18fe1cbac98ae449e0d56e87469103ba08f224a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Visitor/zipball/c18fe1cbac98ae449e0d56e87469103ba08f224a", + "reference": "c18fe1cbac98ae449e0d56e87469103ba08f224a", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Visitor\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Visitor library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "structure", + "visit", + "visitor" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Visitor", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Visitor/issues", + "source": "https://central.hoa-project.net/Resource/Library/Visitor" + }, + "abandoned": true, + "time": "2017-01-16T07:02:03+00:00" + }, + { + "name": "hoa/zformat", + "version": "1.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Zformat.git", + "reference": "522c381a2a075d4b9dbb42eb4592dd09520e4ac2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Zformat/zipball/522c381a2a075d4b9dbb42eb4592dd09520e4ac2", + "reference": "522c381a2a075d4b9dbb42eb4592dd09520e4ac2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Zformat\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Zformat library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "parameter", + "zformat" + ], + "support": { + "docs": "https://central.hoa-project.net/Documentation/Library/Zformat", + "email": "support@hoa-project.net", + "forum": "https://users.hoa-project.net/", + "irc": "irc://chat.freenode.net/hoaproject", + "issues": "https://github.com/hoaproject/Zformat/issues", + "source": "https://central.hoa-project.net/Resource/Library/Zformat" + }, + "abandoned": true, + "time": "2017-01-10T10:39:54+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "b22fb017543bb7147e3bcc53f08fb13a48aff994" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/b22fb017543bb7147e3bcc53f08fb13a48aff994", + "reference": "b22fb017543bb7147e3bcc53f08fb13a48aff994", + "shasum": "" + }, + "require-dev": { + "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", + "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/master" + }, + "time": "2025-04-22T16:22:26+00:00" + }, + { + "name": "nette/bootstrap", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/bootstrap.git", + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/1a7965b4ee401ad0e3f673b9c016d2481afdc280", + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280", + "shasum": "" + }, + "require": { + "nette/di": "^3.0.5", + "nette/utils": "^3.2.1 || ^4.0", + "php": ">=7.2 <8.3" + }, + "conflict": { + "tracy/tracy": "<2.6" + }, + "require-dev": { + "latte/latte": "^2.8", + "nette/application": "^3.1", + "nette/caching": "^3.0", + "nette/database": "^3.0", + "nette/forms": "^3.0", + "nette/http": "^3.0", + "nette/mail": "^3.0", + "nette/robot-loader": "^3.0", + "nette/safe-stream": "^2.2", + "nette/security": "^3.0", + "nette/tester": "^2.0", + "phpstan/phpstan-nette": "^0.12", + "tracy/tracy": "^2.6" + }, + "suggest": { + "nette/robot-loader": "to use Configurator::createRobotLoader()", + "tracy/tracy": "to use Configurator::enableTracy()" + }, + "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 Bootstrap: the simple way to configure and bootstrap your Nette application.", + "homepage": "https://nette.org/", + "keywords": [ + "bootstrapping", + "configurator", + "nette" + ], + "support": { + "issues": "https://github.com/nette/bootstrap/issues", + "source": "https://github.com/nette/bootstrap/tree/v3.1.4" + }, + "time": "2022-12-14T15:23:02+00:00" + }, + { + "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" + }, + { + "name": "nette/finder", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "991aefb42860abeab8e003970c3809a9d83cb932" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/991aefb42860abeab8e003970c3809a9d83cb932", + "reference": "991aefb42860abeab8e003970c3809a9d83cb932", + "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.6-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.6.0" + }, + "time": "2022-10-13T01:31:15+00:00" + }, + { + "name": "nette/neon", + "version": "v3.3.4", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", + "reference": "bb88bf3a54dd21bf4dbddb5cd525d7b0c61b7cda", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "7.1 - 8.4" + }, + "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.4" + }, + "time": "2024-10-04T22:17:24+00:00" + }, + { + "name": "nette/php-generator", + "version": "v3.6.9", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", + "reference": "d31782f7bd2ae84ad06f863391ec3fb77ca4d0a6", + "shasum": "" + }, + "require": { + "nette/utils": "^3.1.2", + "php": ">=7.2 <8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "nikic/php-parser": "^4.13", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6-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/v3.6.9" + }, + "time": "2022-10-04T11:49:47+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.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": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "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.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "ondram/ci-detector", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/OndraM/ci-detector.git", + "reference": "594e61252843b68998bddd48078c5058fe9028bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/594e61252843b68998bddd48078c5058fe9028bd", + "reference": "594e61252843b68998bddd48078c5058fe9028bd", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.2", + "lmc/coding-standard": "^1.3 || ^2.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/extension-installer": "^1.0.3", + "phpstan/phpstan": "^0.12.0", + "phpstan/phpstan-phpunit": "^0.12.1", + "phpunit/phpunit": "^7.1 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OndraM\\CiDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Machulda", + "email": "ondrej.machulda@gmail.com" + } + ], + "description": "Detect continuous integration environment and provide unified access to properties of current build", + "keywords": [ + "CircleCI", + "Codeship", + "Wercker", + "adapter", + "appveyor", + "aws", + "aws codebuild", + "bamboo", + "bitbucket", + "buddy", + "ci-info", + "codebuild", + "continuous integration", + "continuousphp", + "drone", + "github", + "gitlab", + "interface", + "jenkins", + "teamcity", + "travis" + ], + "support": { + "issues": "https://github.com/OndraM/ci-detector/issues", + "source": "https://github.com/OndraM/ci-detector/tree/main" + }, + "time": "2020-09-04T11:21:14+00:00" + }, + { + "name": "ondrejmirtes/better-reflection", + "version": "6.57.0.0", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/BetterReflection.git", + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/dcc22b90a63497f3450dd5eed62197bc46937297", + "reference": "dcc22b90a63497f3450dd5eed62197bc46937297", + "shasum": "" + }, + "require": { + "ext-json": "*", + "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": "^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" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\BetterReflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com", + "homepage": "https://github.com/asgrim" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Gary Hockin", + "email": "gary@roave.com", + "homepage": "https://github.com/geeh" + }, + { + "name": "Jaroslav Hanslík", + "email": "kukulich@kukulich.cz", + "homepage": "https://github.com/kukulich" + } + ], + "description": "Better Reflection - an improved code reflection API", + "support": { + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.57.0.0" + }, + "time": "2025-02-12T21:16:38+00:00" + }, + { + "name": "phpstan/php-8-stubs", + "version": "0.4.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/php-8-stubs.git", + "reference": "d8f8290313e4fd1b4840c553a8492eff31ad54eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/d8f8290313e4fd1b4840c553a8492eff31ad54eb", + "reference": "d8f8290313e4fd1b4840c553a8492eff31ad54eb", + "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.4.12" + }, + "time": "2025-04-15T00:22:00+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "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": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "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/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "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/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://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/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/log", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "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" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-22T16:21:11+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "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": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "0.7.x-dev", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "ce2654d21d2a749e0a6142d00432e65ba003a2d9" + }, + "dist": { + "type": "zip", + "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.4" + }, + "require-dev": { + "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/" + } + }, + "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": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/0.7.x" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-08-04T20:30:51+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "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.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "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/" + } + }, + "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 DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "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": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/http", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/http.git", + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20" + }, + "dist": { + "type": "zip", + "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": "^3 || ^2.3 || ^1.2.1", + "react/socket": "^1.12", + "react/stream": "^1.2" + }, + "require-dev": { + "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/" + } + }, + "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": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": [ + "async", + "client", + "event-driven", + "http", + "http client", + "http server", + "https", + "psr-7", + "reactphp", + "server", + "streaming" + ], + "support": { + "issues": "https://github.com/reactphp/http/issues", + "source": "https://github.com/reactphp/http/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-03-27T17:20:46+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "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": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "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/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "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.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "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/" + } + }, + "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, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "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" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "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/v5.4.47" + }, + "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-11-06T11:30:55+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": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "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": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "63741784cd7b9967975eec610b256eed3ede022b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "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/v5.4.45" + }, + "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-28T13:32:08+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": { + "url": "https://github.com/symfony/polyfill", + "name": "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": { + "url": "https://github.com/symfony/polyfill", + "name": "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": { + "url": "https://github.com/symfony/polyfill", + "name": "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": { + "url": "https://github.com/symfony/polyfill", + "name": "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": { + "url": "https://github.com/symfony/polyfill", + "name": "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/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com/", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/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/process", + "version": "v5.4.40", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com/", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.40" + }, + "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:33:22+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" + }, + "dist": { + "type": "zip", + "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|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "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/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\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/v5.4.47" + }, + "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-11-10T20:33:58+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v6.6.3", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f2d781bb9136cda2f5e73ee778049e80ba681cf6", + "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "jean85/pretty-package-versions": "^2.0.5", + "php": "^7.3 || ^8.0", + "phpunit/php-code-coverage": "^9.2.16", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-timer": "^5.0.3", + "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": "^9.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "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.bat", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "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" + } + }, + "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": "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": "jean85/pretty-package-versions", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "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": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + }, + "time": "2021-10-08T21:21:46+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "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.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.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "ondrejmirtes/simple-downgrader", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/simple-downgrader.git", + "reference": "fb8b7833034f0396d5e4518ed090e3d099b7d9bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/fb8b7833034f0396d5e4518ed090e3d099b7d9bc", + "reference": "fb8b7833034f0396d5e4518ed090e3d099b7d9bc", + "shasum": "" + }, + "require": { + "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": { + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "bin": [ + "bin/simple-downgrade" + ], + "type": "library", + "autoload": { + "psr-4": { + "SimpleDowngrader\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple Downgrader", + "support": { + "issues": "https://github.com/ondrejmirtes/simple-downgrader/issues", + "source": "https://github.com/ondrejmirtes/simple-downgrader/tree/2.0.0" + }, + "time": "2024-10-09T14:55:47+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": "php-parallel-lint/php-parallel-lint", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3.0" + }, + "replace": { + "grogy/php-parallel-lint": "*", + "jakub-onderka/php-parallel-lint": "*" + }, + "require-dev": { + "nette/tester": "^1.3 || ^2.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" + }, + "bin": [ + "parallel-lint" + ], + "type": "library", + "autoload": { + "classmap": [ + "./src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "ahoj@jakubonderka.cz" + } + ], + "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", + "keywords": [ + "lint", + "static analysis" + ], + "support": { + "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": "2024-03-27T12:14:49+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.x-dev", + "source": { + "type": "git", + "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": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "default-branch": true, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "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/2.0.x" + }, + "time": "2025-04-19T16:06:02+00:00" + }, + { + "name": "phpstan/phpstan-nette", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-nette.git", + "reference": "cacb6983bbdf44d5c3a7222e5ca74f61f8531806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/cacb6983bbdf44d5c3a7222e5ca74f61f8531806", + "reference": "cacb6983bbdf44d5c3a7222e5ca74f61f8531806", + "shasum": "" + }, + "require": { + "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": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Nette Framework class reflection extension for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-nette/issues", + "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.0" + }, + "time": "2024-10-26T16:03:48+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "3cc855474263ad6220dfa49167cbea34ca1dd300" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/3cc855474263ad6220dfa49167cbea34ca1dd300", + "reference": "3cc855474263ad6220dfa49167cbea34ca1dd300", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.0" + }, + "time": "2024-10-14T03:16:27+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": "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.23", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "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.23" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "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": "2025-05-02T06:40:34+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": "shipmonk/composer-dependency-analyser", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/bca862b2830a453734aee048eb0cdab82e5c9da3", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "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/composer-dependency-analyser" + ], + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", + "keywords": [ + "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/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.7.0" + }, + "time": "2024-08-08T08:12:32+00:00" + }, + { + "name": "shipmonk/name-collision-detector", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/name-collision-detector.git", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/name-collision-detector/zipball/e8c8267a9a3774450b64f4cbf0bb035108e78f07", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nette/schema": "^1.1.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "phpstan/phpstan": "^1.8.7", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/composer-dependency-analyser": "^1.0.0", + "slevomat/coding-standard": "^8.0.1" + }, + "bin": [ + "bin/detect-collisions" + ], + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\NameCollision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple tool to find ambiguous classes or any other name duplicates within your project.", + "keywords": [ + "ambiguous", + "autoload", + "autoloading", + "classname", + "collision", + "namespace" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/name-collision-detector/issues", + "source": "https://github.com/shipmonk-rnd/name-collision-detector/tree/2.1.1" + }, + "time": "2024-03-01T13:26:32+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": { + "jetbrains/phpstorm-stubs": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.1", + "composer-runtime-api": "^2.0" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.1.99" + }, + "plugin-api-version": "2.6.0" +} diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index f3f31c8c80..22487e357c 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,5 +1,8 @@ parameters: featureToggles: - closureUsesThis: true - randomIntParameters: true - nullCoalesce: true + bleedingEdge: true + checkParameterCastableToNumberFunctions: true + skipCheckGenericClasses!: [] + stricterFunctionMap: true + reportPreciseLineForUnusedFunctionParameter: true + internalTag: true diff --git a/conf/config.level0.neon b/conf/config.level0.neon index ac7867bb3b..24b19d99bf 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -1,93 +1,198 @@ parameters: customRulesetUsed: false - missingClosureNativeReturnCheckObjectTypehint: false - -conditionalTags: - PHPStan\Rules\Functions\ClosureUsesThisRule: - phpstan.rules.rule: %featureToggles.closureUsesThis% - PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule: - phpstan.rules.rule: %checkMissingClosureNativeReturnTypehintRule% - -parametersSchema: - missingClosureNativeReturnCheckObjectTypehint: bool() 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\OffsetAccessWithoutDimForReadingRule - - PHPStan\Rules\Classes\ClassConstantDeclarationRule + - PHPStan\Rules\Cast\UnsetCastRule + - PHPStan\Rules\Classes\AllowedSubTypesRule + - PHPStan\Rules\Classes\ClassAttributesRule + - PHPStan\Rules\Classes\ClassConstantAttributesRule - PHPStan\Rules\Classes\ClassConstantRule - - PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule - - PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule - - PHPStan\Rules\Classes\ExistingClassInClassExtendsRule - - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - - PHPStan\Rules\Classes\InstantiationRule + - PHPStan\Rules\Classes\DuplicateDeclarationRule + - PHPStan\Rules\Classes\EnumSanityRule + - 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\Functions\CallToFunctionParametersRule + - 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\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\Variables\ThisVariableRule + - 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 tags: - phpstan.rules.rule arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule + class: PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule tags: - phpstan.rules.rule arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Functions\CallToNonExistentFunctionRule + class: PHPStan\Rules\Classes\ExistingClassInTraitUseRule tags: - phpstan.rules.rule arguments: - checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Functions\ClosureUsesThisRule + class: PHPStan\Rules\Classes\InstantiationRule + tags: + - phpstan.rules.rule + arguments: + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Methods\CallMethodsRule + class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule tags: - phpstan.rules.rule arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% + checkClassCaseSensitivity: %checkClassCaseSensitivity% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Methods\CallStaticMethodsRule + class: PHPStan\Rules\Functions\CallToNonExistentFunctionRule tags: - phpstan.rules.rule arguments: checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Methods\OverridingMethodRule + class: PHPStan\Rules\Constants\OverridingConstantRule arguments: checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule + class: PHPStan\Rules\Methods\OverridingMethodRule arguments: - checkObjectTypehint: %missingClosureNativeReturnCheckObjectTypehint% + checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + checkMissingOverrideMethodAttribute: %checkMissingOverrideMethodAttribute% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\Missing\MissingReturnRule @@ -103,6 +208,7 @@ services: - phpstan.rules.rule arguments: checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% - class: PHPStan\Rules\Namespaces\ExistingNamesInUseRule @@ -110,33 +216,52 @@ services: - phpstan.rules.rule arguments: checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% + + - + class: PHPStan\Rules\Properties\AccessPropertiesRule + tags: + - phpstan.rules.rule - - class: PHPStan\Rules\Operators\InvalidIncDecOperationRule + class: PHPStan\Rules\Properties\AccessStaticPropertiesRule tags: - phpstan.rules.rule arguments: - checkThisOnly: %checkThisOnly% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Properties\AccessPropertiesRule + class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule tags: - phpstan.rules.rule arguments: - reportMagicProperties: %reportMagicProperties% + checkClassCaseSensitivity: %checkClassCaseSensitivity% + checkThisOnly: %checkThisOnly% + discoveringSymbolsTip: %tips.discoveringSymbols% - - class: PHPStan\Rules\Properties\AccessStaticPropertiesRule + class: PHPStan\Rules\Functions\FunctionCallableRule + arguments: + checkFunctionNameCase: %checkFunctionNameCase% + reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule + class: PHPStan\Rules\Properties\OverridingPropertyRule + arguments: + checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + reportMaybes: %reportMaybesInPropertyPhpDocTypes% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\Properties\SetPropertyHookParameterRule arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - checkThisOnly: %checkThisOnly% + checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + checkMissingTypehints: %checkMissingTypehints% + tags: + - phpstan.rules.rule - class: PHPStan\Rules\Properties\WritingToReadOnlyPropertiesRule @@ -168,13 +293,17 @@ services: - phpstan.rules.rule - - class: PHPStan\Rules\Variables\DefinedVariableInAnonymousFunctionUseRule + class: PHPStan\Rules\Keywords\RequireFileExistsRule arguments: - checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% + currentWorkingDirectory: %currentWorkingDirectory% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Regexp\RegularExpressionPatternRule - 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 2499484180..a1e3872d6b 100644 --- a/conf/config.level1.neon +++ b/conf/config.level1.neon @@ -7,17 +7,17 @@ parameters: reportMagicMethods: true reportMagicProperties: true - -conditionalTags: - PHPStan\Rules\Variables\VariableCertaintyNullCoalesceRule: - phpstan.rules.rule: %featureToggles.nullCoalesce% - rules: - PHPStan\Rules\Classes\UnusedConstructorParametersRule - - PHPStan\Rules\Constants\ConstantRule - PHPStan\Rules\Functions\UnusedClosureUsesRule - - PHPStan\Rules\Variables\VariableCertaintyInIssetRule + - PHPStan\Rules\Variables\EmptyRule + - PHPStan\Rules\Variables\IssetRule + - PHPStan\Rules\Variables\NullCoalesceRule services: - - class: PHPStan\Rules\Variables\VariableCertaintyNullCoalesceRule + 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 9ff871b3d4..51214ea554 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -11,44 +11,114 @@ rules: - PHPStan\Rules\Cast\InvalidCastRule - 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 + - PHPStan\Rules\Generics\EnumTemplateTypeRule - PHPStan\Rules\Generics\FunctionTemplateTypeRule - PHPStan\Rules\Generics\FunctionSignatureVarianceRule - 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 afe67c44f9..4e5f80c5ef 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -2,19 +2,29 @@ includes: - config.level2.neon rules: - - PHPStan\Rules\Arrays\AppendedArrayItemTypeRule + - 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: @@ -22,37 +32,47 @@ parameters: services: - - class: PHPStan\Rules\Arrays\AppendedArrayKeyTypeRule + class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule arguments: - checkUnionTypes: %checkUnionTypes% + reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule + class: PHPStan\Rules\Arrays\InvalidKeyInArrayItemRule arguments: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Arrays\InvalidKeyInArrayItemRule + class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchRule arguments: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchRule + class: PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule arguments: - reportMaybes: %reportMaybes% + exceptionTypeResolver: @exceptionTypeResolver + missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Exceptions\ThrowsVoidMethodWithExplicitThrowPointRule + arguments: + exceptionTypeResolver: @exceptionTypeResolver + missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Functions\ReturnTypeRule + class: PHPStan\Rules\Exceptions\ThrowsVoidPropertyHookWithExplicitThrowPointRule arguments: - functionReflector: @betterReflectionFunctionReflector + exceptionTypeResolver: @exceptionTypeResolver + missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows% tags: - phpstan.rules.rule diff --git a/conf/config.level4.neon b/conf/config.level4.neon index a879768acd..b026238cfb 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -3,29 +3,47 @@ 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\Exceptions\DeadCatchRule - - PHPStan\Rules\Functions\CallToFunctionStamentWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToMethodStamentWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToStaticMethodStamentWithoutSideEffectsRule + - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule + - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule + - 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\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\Variables\IssetRule: - phpstan.rules.rule: %featureToggles.nullCoalesce% - PHPStan\Rules\Variables\NullCoalesceRule: - phpstan.rules.rule: %featureToggles.nullCoalesce% + 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 services: - class: PHPStan\Rules\Classes\ImpossibleInstanceOfRule arguments: - checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -33,6 +51,8 @@ services: class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -40,6 +60,8 @@ services: class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -47,6 +69,60 @@ 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: + alwaysWrittenTags: %propertyAlwaysWrittenTags% + alwaysReadTags: %propertyAlwaysReadTags% + checkUninitializedProperties: %checkUninitializedProperties% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Comparison\DoWhileLoopConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -54,6 +130,8 @@ services: class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -61,37 +139,77 @@ 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: + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule arguments: - checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Comparison\ConstantLooseComparisonRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -99,23 +217,35 @@ services: class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Comparison\UnreachableIfBranchesRule + class: PHPStan\Rules\Comparison\WhileLoopAlwaysFalseConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Comparison\UnreachableTernaryElseBranchRule + 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: @@ -124,7 +254,24 @@ services: - phpstan.rules.rule - - class: PHPStan\Rules\Variables\IssetRule + class: PHPStan\Rules\Properties\NullsafePropertyFetchRule + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Traits\TraitDeclarationCollector + tags: + - phpstan.collector - - class: PHPStan\Rules\Variables\NullCoalesceRule + 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 a80147786e..fd3835fbf1 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -1,17 +1,44 @@ includes: - config.level4.neon -conditionalTags: - PHPStan\Rules\Functions\RandomIntParametersRule: - phpstan.rules.rule: %featureToggles.randomIntParameters% - parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true +conditionalTags: + PHPStan\Rules\Functions\ParameterCastableToNumberRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions% + +rules: + - PHPStan\Rules\DateTimeInstantiationRule + - PHPStan\Rules\Functions\CallUserFuncRule + - PHPStan\Rules\Functions\ParameterCastableToStringRule + - PHPStan\Rules\Functions\ImplodeParameterCastableToStringRule + - PHPStan\Rules\Functions\SortParameterCastableToStringRule + - PHPStan\Rules\Regexp\RegularExpressionQuotingRule services: - class: PHPStan\Rules\Functions\RandomIntParametersRule arguments: 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 4fdeb19345..25214e97dd 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -2,14 +2,14 @@ includes: - config.level5.neon parameters: - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingVarTagTypehint: true checkMissingTypehints: true rules: + - PHPStan\Rules\Constants\MissingClassConstantTypehintRule - PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule - 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.level9.neon b/conf/config.level9.neon new file mode 100644 index 0000000000..efb3e30ffa --- /dev/null +++ b/conf/config.level9.neon @@ -0,0 +1,5 @@ +includes: + - config.level8.neon + +parameters: + checkExplicitMixed: true diff --git a/conf/config.levelmax.neon b/conf/config.levelmax.neon index 789cb34109..ce4c43f2f7 100644 --- a/conf/config.levelmax.neon +++ b/conf/config.levelmax.neon @@ -1,2 +1,2 @@ includes: - - config.level8.neon + - config.level10.neon diff --git a/conf/config.neon b/conf/config.neon index 937b9633cb..a0083d7753 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1,102 +1,152 @@ +includes: + - parametersSchema.neon + parameters: - bootstrap: null - bootstrapFiles: [] - excludes_analyse: [] - autoload_directories: [] - autoload_files: [] + bootstrapFiles: + - ../stubs/runtime/ReflectionUnionType.php + - ../stubs/runtime/ReflectionAttribute.php + - ../stubs/runtime/Attribute.php + - ../stubs/runtime/ReflectionIntersectionType.php + excludePaths: [] level: null paths: [] + exceptions: + implicitThrows: true + reportUncheckedExceptionDeadCatch: true + uncheckedExceptionRegexes: [] + uncheckedExceptionClasses: [] + checkedExceptionRegexes: [] + checkedExceptionClasses: [] + check: + missingCheckedExceptionInThrows: false + tooWideThrowType: true featureToggles: - disableRuntimeReflectionProvider: false - closureUsesThis: false - randomIntParameters: false - nullCoalesce: false + bleedingEdge: false + checkParameterCastableToNumberFunctions: false + skipCheckGenericClasses: [] + stricterFunctionMap: false + reportPreciseLineForUnusedFunctionParameter: false + internalTag: false fileExtensions: - php - checkAlwaysTrueCheckTypeFunctionCall: false - checkAlwaysTrueInstanceof: false - checkAlwaysTrueStrictComparison: false + checkAdvancedIsset: false + reportAlwaysTrueInLastCondition: false checkClassCaseSensitivity: false checkExplicitMixed: false + checkImplicitMixed: false checkFunctionArgumentTypes: false checkFunctionNameCase: false - checkGenericClassInNonGenericObjectType: false - checkMissingIterableValueType: false + checkInternalClassCaseSensitivity: false + checkMissingCallableSignature: false checkMissingVarTagTypehint: false checkArgumentsPassedByReference: false checkMaybeUndefinedVariables: false checkNullables: false checkThisOnly: true checkUnionTypes: false + checkBenevolentUnionTypes: false checkExplicitMixedMissingReturn: false checkPhpDocMissingReturn: false checkPhpDocMethodSignatures: false checkExtraArguments: false - checkMissingClosureNativeReturnTypehintRule: false 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: [] parallel: jobSize: 20 - processTimeout: 60.0 + processTimeout: 600.0 maximumNumberOfProcesses: 32 minimumNumberOfJobsPerProcess: 2 buffer: 134217728 # 128 MB phpVersion: null polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true - polluteCatchScopeWithTryAssignments: false + 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: 512 - nodesByStringCountMax: 512 + nodesByStringCountMax: 256 reportUnmatchedIgnoredErrors: true - scopeClass: PHPStan\Analyser\MutatingScope - typeAliases: - scalar: 'int|float|string|bool' - number: 'int|float' + typeAliases: [] universalObjectCratesClasses: - stdClass stubFiles: - - ../stubs/ReflectionClass.stub + - ../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/ReflectionFunctionAbstract.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 - staticReflectionClassNamePatterns: - - '#^PhpParser\\#' - - '#^PHPStan\\#' - - '#^Hoa\\#' + resultCachePath: %tmpDir%/resultCache.php + resultCacheSkipIfOlderThanDays: 7 + resultCacheChecksProjectExtensionFilesDependencies: false 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 - PHP_RELEASE_VERSION - PHP_VERSION_ID - PHP_EXTRA_VERSION + - PHP_WINDOWS_VERSION_MAJOR + - PHP_WINDOWS_VERSION_MINOR + - PHP_WINDOWS_VERSION_BUILD - PHP_ZTS - PHP_DEBUG - PHP_MAXPATHLEN @@ -127,144 +177,182 @@ parameters: - PHP_CONFIG_FILE_SCAN_DIR - PHP_SHLIB_SUFFIX - PHP_FD_SETSIZE + - 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 - -parametersSchema: - bootstrap: schema(string(), nullable()) - bootstrapFiles: listOf(string()) - excludes_analyse: listOf(string()) - autoload_directories: listOf(string()) - autoload_files: listOf(string()) - level: schema(anyOf(int(), string()), nullable()) - paths: listOf(string()) - featureToggles: structure([ - disableRuntimeReflectionProvider: bool(), - closureUsesThis: bool(), - randomIntParameters: bool(), - nullCoalesce: bool() - ]) - fileExtensions: listOf(string()) - checkAlwaysTrueCheckTypeFunctionCall: bool() - checkAlwaysTrueInstanceof: bool() - checkAlwaysTrueStrictComparison: bool() - checkClassCaseSensitivity: bool() - checkExplicitMixed: bool() - checkFunctionArgumentTypes: bool() - checkFunctionNameCase: bool() - checkGenericClassInNonGenericObjectType: bool() - checkMissingIterableValueType: bool() - checkMissingVarTagTypehint: bool() - checkArgumentsPassedByReference: bool() - checkMaybeUndefinedVariables: bool() - checkNullables: bool() - checkThisOnly: bool() - checkUnionTypes: bool() - checkExplicitMixedMissingReturn: bool() - checkPhpDocMissingReturn: bool() - checkPhpDocMethodSignatures: bool() - checkExtraArguments: bool() - checkMissingClosureNativeReturnTypehintRule: bool() - checkMissingTypehints: bool() - checkTooWideReturnTypesInProtectedAndPublicMethods: bool() - inferPrivatePropertyTypeFromConstructor: bool() - tipsOfTheDay: bool() - reportMaybes: bool() - reportMaybesInMethodSignatures: bool() - reportStaticMethodSignatures: bool() - parallel: structure([ - jobSize: int(), - processTimeout: float(), - maximumNumberOfProcesses: int(), - minimumNumberOfJobsPerProcess: int(), - buffer: int() - ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80000))), nullable()) - polluteScopeWithLoopInitialAssignments: bool() - polluteScopeWithAlwaysIterableForeach: bool() - polluteCatchScopeWithTryAssignments: bool() - 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() - staticReflectionClassNamePatterns: listOf(string()) - dynamicConstantNames: listOf(string()) - customRulesetUsed: bool() - rootDir: string() - tmpDir: string() - currentWorkingDirectory: string() - cliArgumentsVariablesRegistered: bool() - mixinExcludeClasses: listOf(string()) - scanFiles: listOf(string()) - scanDirectories: listOf(string()) - - # irrelevant Nette parameters - debugMode: bool() - productionMode: bool() - tempDir: string() - - # internal parameters only for DerivativeContainerFactory - additionalConfigFiles: listOf(string()) - allCustomConfigFiles: listOf(string()) - analysedPaths: listOf(string()) - composerAutoloaderProjectPaths: listOf(string()) - analysedPathsFromConfig: listOf(string()) - usedLevel: string() - cliAutoloadFile: schema(string(), nullable()) + validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension + 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\MissingCheckedExceptionInPropertyHookThrowsRule: + phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% + PHPStan\Rules\Properties\UninitializedPropertyRule: + phpstan.rules.rule: %checkUninitializedProperties% services: - class: PhpParser\BuilderFactory - - class: PhpParser\Lexer\Emulative + class: PHPStan\Parser\LexerFactory - - class: PhpParser\NodeTraverser - setup: - - addVisitor(@PhpParser\NodeVisitor\NameResolver) + class: PhpParser\NodeVisitor\NameResolver + arguments: + options: + preserveOriginalNames: true - - class: PhpParser\NodeVisitor\NameResolver + 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: PhpParser\Parser\Php7 + class: PHPStan\Parser\TryCatchTypeVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\LastConditionVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + 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: PhpParser\PrettyPrinter\Standard + class: PHPStan\Node\Printer\Printer + autowired: + - PHPStan\Node\Printer\Printer - class: PHPStan\Broker\AnonymousClassNameHelper @@ -277,8 +365,24 @@ services: - class: PHPStan\Php\PhpVersionFactory + factory: @PHPStan\Php\PhpVersionFactoryFactory::create + + - + class: PHPStan\Php\PhpVersionFactoryFactory + arguments: + phpVersion: %phpVersion% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + + - + class: PHPStan\Php\ComposerPhpVersionFactory + arguments: + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + + - + class: PHPStan\PhpDocParser\ParserConfig arguments: - versionId: %phpVersion% + usedAttributes: + lines: true - class: PHPStan\PhpDocParser\Lexer\Lexer @@ -292,6 +396,9 @@ services: - class: PHPStan\PhpDocParser\Parser\PhpDocParser + - + class: PHPStan\PhpDocParser\Printer\Printer + - class: PHPStan\PhpDoc\PhpDocInheritanceResolver @@ -304,13 +411,6 @@ services: - class: PHPStan\PhpDoc\ConstExprNodeResolver - - - class: PHPStan\PhpDoc\TypeAlias\TypeAliasesTypeNodeResolverExtension - arguments: - aliases: %typeAliases% - tags: - - phpstan.phpDoc.typeNodeResolverExtension - - class: PHPStan\PhpDoc\TypeNodeResolver @@ -323,8 +423,34 @@ 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 @@ -332,65 +458,132 @@ services: internalErrorsCountLimit: %internalErrorsCountLimit% - - class: PHPStan\Analyser\FileAnalyser + class: PHPStan\Analyser\AnalyserResultFinalizer arguments: reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - class: PHPStan\Analyser\IgnoredErrorHelper + class: PHPStan\Analyser\FileAnalyser + arguments: + parser: @defaultAnalysisParser + + - + 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 arguments: - classReflector: @nodeScopeResolverClassReflector + parser: @defaultAnalysisParser + reflector: @nodeScopeResolverReflector polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments% - polluteCatchScopeWithTryAssignments: %polluteCatchScopeWithTryAssignments% 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\ResultCache\ResultCacheManager + class: PHPStan\Analyser\ConstantResolverFactory + + - + implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory arguments: - cacheFilePath: %tmpDir%/resultCache.php - allCustomConfigFiles: %allCustomConfigFiles% + scanFileFinder: @fileFinderScan + cacheFilePath: %resultCachePath% 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% + + - + 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 + + - + class: PHPStan\Command\AnalyserRunner + + - + class: PHPStan\Command\FixerApplication arguments: - memoryLimitFile: %memoryLimitFile% - internalErrorsCountLimit: %internalErrorsCountLimit% + analysedPaths: %analysedPaths% + currentWorkingDirectory: %currentWorkingDirectory% + proTmpDir: %pro.tmpDir% + dnsServers: %pro.dnsServers% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + allConfigFiles: %allConfigFiles% + cliAutoloadFile: %cliAutoloadFile% + bootstrapFiles: %bootstrapFiles% + editorUrl: %editorUrl% + usedLevel: %usedLevel% - - class: PHPStan\Command\IgnoredRegexValidator + class: PHPStan\Dependency\DependencyResolver + + - + class: PHPStan\Dependency\ExportedNodeFetcher arguments: - parser: @regexParser + parser: @defaultAnalysisParser - - class: PHPStan\Dependency\DependencyDumper + class: PHPStan\Dependency\ExportedNodeResolver - - class: PHPStan\Dependency\DependencyResolver + class: PHPStan\Dependency\ExportedNodeVisitor - class: PHPStan\DependencyInjection\Container @@ -412,8 +605,9 @@ services: analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% analysedPathsFromConfig: %analysedPathsFromConfig% - allCustomConfigFiles: %allCustomConfigFiles% usedLevel: %usedLevel% + generateBaselineFile: %generateBaselineFile% + cliAutoloadFile: %cliAutoloadFile% - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider @@ -423,25 +617,77 @@ 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 + - + 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: workingDirectory: %currentWorkingDirectory% - - class: PHPStan\File\FileExcluder + class: PHPStan\File\FileExcluderFactory arguments: - analyseExcludes: %excludes_analyse% - stubFiles: %stubFiles% + excludePaths: %excludePaths% - + implement: PHPStan\File\FileExcluderRawFactory + + fileExcluderAnalyse: + class: PHPStan\File\FileExcluder + factory: @PHPStan\File\FileExcluderFactory::createAnalyseFileExcluder() + autowired: false + + fileExcluderScan: + class: PHPStan\File\FileExcluder + factory: @PHPStan\File\FileExcluderFactory::createScanFileExcluder() + autowired: false + + fileFinderAnalyse: + class: PHPStan\File\FileFinder + arguments: + fileExcluder: @fileExcluderAnalyse + fileExtensions: %fileExtensions% + autowired: false + + fileFinderScan: class: PHPStan\File\FileFinder arguments: + fileExcluder: @fileExcluderScan fileExtensions: %fileExtensions% + autowired: false + + - + class: PHPStan\File\FileMonitor + arguments: + fileFinder: @fileFinderAnalyse + + - + class: PHPStan\Parser\DeclarePositionVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ImmediatelyInvokedClosureVisitor + tags: + - phpstan.parser.richParserNodeVisitor - class: PHPStan\Parallel\ParallelAnalyser @@ -456,19 +702,24 @@ services: jobSize: %parallel.jobSize% maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% minimumNumberOfJobsPerProcess: %parallel.minimumNumberOfJobsPerProcess% + tags: + - phpstan.diagnoseExtension - - class: PHPStan\Parser\CachedParser - arguments: - originalParser: @directParser - cachedNodesByFileCountMax: %cache.nodesByFileCountMax% - cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + class: PHPStan\Process\CpuCoreCounter - - class: PHPStan\Parser\FunctionCallStatementFinder + class: PHPStan\Reflection\AttributeReflectionFactory - implement: PHPStan\Reflection\FunctionReflectionFactory + arguments: + parser: @defaultAnalysisParser + + - + class: PHPStan\Reflection\InitializerExprTypeResolver + arguments: + usePathConstantsAsConstantString: %usePathConstantsAsConstantString% - class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension @@ -482,16 +733,15 @@ services: - class: PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher arguments: - phpParser: @phpParserDecorator - - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator + parser: @defaultAnalysisParser - class: PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker - - implement: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory + class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory + arguments: + fileFinder: @fileFinderScan - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository @@ -506,27 +756,65 @@ 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% - class: PHPStan\Reflection\Php\PhpClassReflectionExtension arguments: + parser: @defaultAnalysisParser inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% - universalObjectCratesClasses: %universalObjectCratesClasses% - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory + arguments: + parser: @defaultAnalysisParser + + - + class: PHPStan\Reflection\Php\Soap\SoapClientMethodsClassReflectionExtension + tags: + - phpstan.broker.methodsClassReflectionExtension + + - + class: PHPStan\Reflection\Php\EnumAllowedSubTypesClassReflectionExtension + tags: + - phpstan.broker.allowedSubTypesClassReflectionExtension - class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension @@ -535,69 +823,211 @@ 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 - class: PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider + arguments: + reflector: @betterReflectionReflector - class: PHPStan\Reflection\SignatureMap\SignatureMapParser - - class: PHPStan\Reflection\SignatureMap\SignatureMapProvider + class: PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider + arguments: + stricterFunctionMap: %featureToggles.stricterFunctionMap% + autowired: + - PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider - - class: PHPStan\Rules\ClassCaseSensitivityCheck + class: PHPStan\Reflection\SignatureMap\Php8SignatureMapProvider + autowired: + - PHPStan\Reflection\SignatureMap\Php8SignatureMapProvider - - class: PHPStan\Rules\Comparison\ConstantConditionRuleHelper - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + class: PHPStan\Reflection\SignatureMap\SignatureMapProviderFactory - - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper - arguments: - universalObjectCratesClasses: %universalObjectCratesClasses% - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + class: PHPStan\Reflection\SignatureMap\SignatureMapProvider + factory: @PHPStan\Reflection\SignatureMap\SignatureMapProviderFactory::create() - - class: PHPStan\Rules\FunctionCallParametersCheck + class: PHPStan\Rules\Api\ApiRuleHelper + + - + class: PHPStan\Rules\AttributesCheck arguments: - checkArgumentTypes: %checkFunctionArgumentTypes% - checkArgumentsPassedByReference: %checkArgumentsPassedByReference% - checkExtraArguments: %checkExtraArguments% - checkMissingTypehints: %checkMissingTypehints% + deprecationRulesInstalled: %deprecationRulesInstalled% - - class: PHPStan\Rules\FunctionDefinitionCheck + class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchCheck arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - checkThisOnly: %checkThisOnly% + 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: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper + arguments: + universalObjectCratesClasses: %universalObjectCratesClasses% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver + arguments: + uncheckedExceptionRegexes: %exceptions.uncheckedExceptionRegexes% + uncheckedExceptionClasses: %exceptions.uncheckedExceptionClasses% + checkedExceptionRegexes: %exceptions.checkedExceptionRegexes% + checkedExceptionClasses: %exceptions.checkedExceptionClasses% + autowired: + - PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver + + - + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule + + - + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInMethodThrowsRule + + - + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule + + - + class: PHPStan\Rules\Exceptions\MissingCheckedExceptionInThrowsCheck + arguments: + exceptionTypeResolver: @exceptionTypeResolver + + - + class: PHPStan\Rules\Exceptions\TooWideThrowTypeCheck + arguments: + implicitThrows: %exceptions.implicitThrows% + + - + class: PHPStan\Rules\FunctionCallParametersCheck + arguments: + checkArgumentTypes: %checkFunctionArgumentTypes% + checkArgumentsPassedByReference: %checkArgumentsPassedByReference% + checkExtraArguments: %checkExtraArguments% + checkMissingTypehints: %checkMissingTypehints% + + - + class: PHPStan\Rules\FunctionDefinitionCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + checkThisOnly: %checkThisOnly% - class: PHPStan\Rules\FunctionReturnTypeCheck + - + class: PHPStan\Rules\ParameterCastableToStringCheck + + - + class: PHPStan\Rules\Generics\CrossCheckInterfacesHelper - 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: checkClassCaseSensitivity: %checkClassCaseSensitivity% - typeAliases: %typeAliases% - class: PHPStan\Rules\Generics\VarianceCheck + - + class: PHPStan\Rules\InternalTag\RestrictedInternalUsageHelper + - class: PHPStan\Rules\IssetCheck + arguments: + checkAdvancedIsset: %checkAdvancedIsset% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\Methods\MethodCallCheck + arguments: + checkFunctionNameCase: %checkFunctionNameCase% + reportMagicMethods: %reportMagicMethods% + + - + class: PHPStan\Rules\Methods\StaticMethodCallCheck + arguments: + checkFunctionNameCase: %checkFunctionNameCase% + discoveringSymbolsTip: %tips.discoveringSymbols% + reportMagicMethods: %reportMagicMethods% - # checked as part of OverridingMethodRule @@ -606,11 +1036,69 @@ 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% + + - + class: PHPStan\Rules\NullsafeCheck + + - + 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 - class: PHPStan\Rules\Properties\PropertyDescriptor @@ -619,7 +1107,7 @@ services: class: PHPStan\Rules\Properties\PropertyReflectionFinder - - class: PHPStan\Rules\RegistryFactory + class: PHPStan\Rules\Pure\FunctionPurityCheck - class: PHPStan\Rules\RuleLevelHelper @@ -628,18 +1116,85 @@ 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 + arguments: + globalTypeAliases: %typeAliases% + + - + class: PHPStan\Type\TypeAliasResolverProvider + factory: PHPStan\Type\LazyTypeAliasResolverProvider + + - + 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 + + - + class: PHPStan\Type\Php\ArrayColumnHelper + + - + class: PHPStan\Type\Php\ArrayColumnFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayCombineFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayCurrentDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayFillFunctionReturnTypeExtension tags: @@ -651,7 +1206,25 @@ services: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeReturnTypeExtension + class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeHelper + + - + class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayFlipFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayFindFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayFindKeyFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -681,193 +1254,558 @@ services: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ArrayMapFunctionReturnTypeExtension + class: PHPStan\Type\Php\ArrayMapFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayMergeFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayNextDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayPopFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayRandFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayReduceFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayReplaceFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayReverseFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayShiftFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArraySliceFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArraySpliceFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArraySearchFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArraySearchFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + + - + class: PHPStan\Type\Php\ArrayValuesFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArraySumFunctionDynamicReturnTypeExtension + 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: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\BcMathStringOrNullReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ClosureBindDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ClosureBindToDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ClosureFromCallableDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\CompactFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + 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\CurlGetinfoFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\DateFunctionReturnTypeHelper + + - + class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\DateFormatMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\DateFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\DateIntervalConstructorThrowTypeExtension + 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: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ExplodeFunctionDynamicReturnTypeExtension + 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: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\GetClassDynamicReturnTypeExtension + 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: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\HashFunctionsReturnTypeExtension + 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: + - phpstan.dynamicStaticMethodThrowTypeExtension + + - + class: PHPStan\Type\Php\ReflectionFunctionConstructorThrowTypeExtension + tags: + - phpstan.dynamicStaticMethodThrowTypeExtension + + - + class: PHPStan\Type\Php\ReflectionMethodConstructorThrowTypeExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.dynamicStaticMethodThrowTypeExtension - - class: PHPStan\Type\Php\ArrayMergeFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\ReflectionPropertyConstructorThrowTypeExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.dynamicStaticMethodThrowTypeExtension - - class: PHPStan\Type\Php\ArrayPopFunctionReturnTypeExtension + class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\ArrayReduceFunctionReturnTypeExtension + class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.broker.propertiesClassReflectionExtension - - class: PHPStan\Type\Php\ArrayShiftFunctionReturnTypeExtension + class: PHPStan\Type\Php\SimpleXMLElementConstructorThrowTypeExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.dynamicStaticMethodThrowTypeExtension - - class: PHPStan\Type\Php\ArraySliceFunctionReturnTypeExtension + class: PHPStan\Type\Php\StatDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Type\Php\ArraySearchFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\MethodExistsTypeSpecifyingExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\ArrayValuesFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\PropertyExistsTypeSpecifyingExtension tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\Base64DecodeDynamicFunctionReturnTypeExtension + class: PHPStan\Type\Php\MinMaxFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ClosureBindDynamicReturnTypeExtension + class: PHPStan\Type\Php\NumberFormatFunctionDynamicReturnTypeExtension tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ClosureBindToDynamicReturnTypeExtension + class: PHPStan\Type\Php\PathinfoFunctionDynamicReturnTypeExtension tags: - - phpstan.broker.dynamicMethodReturnTypeExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ClosureFromCallableDynamicReturnTypeExtension + class: PHPStan\Type\Php\PregFilterFunctionReturnTypeExtension tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\CountFunctionReturnTypeExtension + class: PHPStan\Type\Php\PregSplitDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\CountFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\ReflectionClassIsSubclassOfTypeSpecifyingExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - class: PHPStan\Type\Php\CurlInitReturnTypeExtension + class: PHPStan\Type\Php\ReplaceFunctionsDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\DsMapDynamicReturnTypeExtension + class: PHPStan\Type\Php\ArrayPointerFunctionsDynamicReturnTypeExtension tags: - - phpstan.broker.dynamicMethodReturnTypeExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\DioStatDynamicFunctionReturnTypeExtension + class: PHPStan\Type\Php\LtrimFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ExplodeFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\MbFunctionsReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension + class: PHPStan\Type\Php\MbConvertEncodingFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\GetCalledClassDynamicReturnTypeExtension + class: PHPStan\Type\Php\MbSubstituteCharacterDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\GetClassDynamicReturnTypeExtension + class: PHPStan\Type\Php\MbStrlenFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension + class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension + class: PHPStan\Type\Php\HrtimeFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension + class: PHPStan\Type\Php\ImplodeFunctionReturnTypeExtension tags: - - phpstan.broker.propertiesClassReflectionExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\StatDynamicReturnTypeExtension + class: PHPStan\Type\Php\NonEmptyStringFunctionsReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Type\Php\MethodExistsTypeSpecifyingExtension + class: PHPStan\Type\Php\SetTypeFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\PropertyExistsTypeSpecifyingExtension + class: PHPStan\Type\Php\StrCaseFunctionsReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\MinMaxFunctionReturnTypeExtension + class: PHPStan\Type\Php\StrlenFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\PathinfoFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\StrIncrementDecrementFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ReplaceFunctionsDynamicReturnTypeExtension + class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ArrayPointerFunctionsDynamicReturnTypeExtension + class: PHPStan\Type\Php\StrRepeatFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\VarExportFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\MbFunctionsReturnTypeExtension + class: PHPStan\Type\Php\ThrowableReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\MbConvertEncodingFunctionReturnTypeExtension + class: PHPStan\Type\Php\TriggerErrorDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension + class: PHPStan\Type\Php\TrimFunctionDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\HrtimeFunctionReturnTypeExtension + class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension + arguments: + configPhpVersion: %phpVersion% tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\PowFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\RoundFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -896,6 +1834,11 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\ClassImplementsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension tags: @@ -907,116 +1850,170 @@ services: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\InArrayFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\FunctionExistsFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsIntFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\InArrayFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsFloatFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IsArrayFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsNullFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IsCallableFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsArrayFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IsIterableFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsBoolFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IsSubclassOfFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsCallableFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IteratorToArrayFunctionReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\IsCountableFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsResourceFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper + + - + class: PHPStan\Type\Php\CtypeDigitFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsIterableFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\JsonThrowOnErrorDynamicReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\IsStringFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\TypeSpecifyingFunctionsDynamicReturnTypeExtension + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + universalObjectCratesClasses: %universalObjectCratesClasses% tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\IsSubclassOfFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\SimpleXMLElementAsXMLMethodReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Type\Php\IsObjectFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\SimpleXMLElementXpathMethodReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Type\Php\IsNumericFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\StrSplitFunctionReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\IsScalarFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\StrTokFunctionReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingExtension + class: PHPStan\Type\Php\SprintfFunctionDynamicReturnTypeExtension tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\JsonThrowOnErrorDynamicReturnTypeExtension + class: PHPStan\Type\Php\SscanfFunctionDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\TypeSpecifyingFunctionsDynamicReturnTypeExtension - arguments: - treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + class: PHPStan\Type\Php\StrvalFamilyFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\SimpleXMLElementAsXMLMethodReturnTypeExtension + class: PHPStan\Type\Php\StrWordCountFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\XMLReaderOpenReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - class: PHPStan\Type\Php\StrSplitFunctionReturnTypeExtension + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionClass tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Type\Php\SprintfFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionClassConstant tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.broker.dynamicMethodReturnTypeExtension - - class: PHPStan\Type\Php\StrWordCountFunctionDynamicReturnTypeExtension + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionFunctionAbstract tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionParameter + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionProperty + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\DatePeriodConstructorReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + + - + 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 typeSpecifier: class: PHPStan\Analyser\TypeSpecifier @@ -1031,6 +2028,7 @@ services: arguments: currentWorkingDirectory: %currentWorkingDirectory% analysedPaths: %analysedPaths% + fallbackRelativePathHelper: @parentDirectoryRelativePathHelper simpleRelativePathHelper: class: PHPStan\File\RelativePathHelper @@ -1039,14 +2037,11 @@ services: currentWorkingDirectory: %currentWorkingDirectory% autowired: false - broker: - class: PHPStan\Broker\Broker - factory: @brokerFactory::create - autowired: - - PHPStan\Broker\Broker - - brokerFactory: - class: PHPStan\Broker\BrokerFactory + parentDirectoryRelativePathHelper: + class: PHPStan\File\ParentDirectoryRelativePathHelper + arguments: + parentDirectory: %currentWorkingDirectory% + autowired: false cacheStorage: class: PHPStan\Cache\FileCacheStorage @@ -1054,33 +2049,69 @@ services: directory: %tmpDir%/cache/PHPStan autowired: no - directParser: - class: PHPStan\Parser\DirectParser + currentPhpVersionRichParser: + class: PHPStan\Parser\RichParser + arguments: + parser: @currentPhpVersionPhpParser + autowired: no + + currentPhpVersionSimpleParser: + class: PHPStan\Parser\CleaningParser + arguments: + wrappedParser: @currentPhpVersionSimpleDirectParser + autowired: no + + currentPhpVersionSimpleDirectParser: + class: PHPStan\Parser\SimpleParser + arguments: + parser: @currentPhpVersionPhpParser autowired: no + defaultAnalysisParser: + class: PHPStan\Parser\CachedParser + arguments: + originalParser: @pathRoutingParser + cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + autowired: false + phpParserDecorator: class: PHPStan\Parser\PhpParserDecorator arguments: - wrappedParser: @PHPStan\Parser\Parser - autowired: no + wrappedParser: @defaultAnalysisParser + autowired: false + + currentPhpVersionLexer: + class: PhpParser\Lexer + factory: @PHPStan\Parser\LexerFactory::create() + autowired: false + + currentPhpVersionPhpParser: + 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: - 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 @@ -1088,102 +2119,133 @@ services: - PHPStan\Reflection\ReflectionProvider betterReflectionSourceLocator: - class: Roave\BetterReflection\SourceLocator\Type\SourceLocator + class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator factory: @PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory::create autowired: false - betterReflectionClassReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingClassReflector + originalBetterReflectionReflector: + class: PHPStan\BetterReflection\Reflector\DefaultReflector arguments: sourceLocator: @betterReflectionSourceLocator - autowired: false - - nodeScopeResolverClassReflector: - factory: @betterReflectionClassReflector - autowired: false - betterReflectionFunctionReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingFunctionReflector + betterReflectionReflector: + class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingReflector arguments: - classReflector: @betterReflectionClassReflector - sourceLocator: @betterReflectionSourceLocator + reflector: @originalBetterReflectionReflector autowired: false - betterReflectionConstantReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingConstantReflector - arguments: - classReflector: @betterReflectionClassReflector - sourceLocator: @betterReflectionSourceLocator + nodeScopeResolverReflector: + factory: @betterReflectionReflector autowired: false betterReflectionProvider: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: - classReflector: @betterReflectionClassReflector - functionReflector: @betterReflectionFunctionReflector - constantReflector: @betterReflectionConstantReflector - autowired: false - - regexParser: - class: Hoa\Compiler\Llk\Parser - factory: Hoa\Compiler\Llk\Llk::load(@regexGrammarStream) - - regexGrammarStream: - class: Hoa\File\Read - arguments: - streamName: 'hoa://Library/Regex/Grammar.pp' - - runtimeReflectionProvider: - class: PHPStan\Reflection\ReflectionProvider\ClassBlacklistReflectionProvider - arguments: - reflectionProvider: @innerRuntimeReflectionProvider - patterns: %staticReflectionClassNamePatterns% + reflector: @betterReflectionReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false - innerRuntimeReflectionProvider: - class: PHPStan\Reflection\Runtime\RuntimeReflectionProvider - - class: PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory arguments: parser: @phpParserDecorator - autoloadDirectories: %autoload_directories% - autoloadFiles: %autoload_files% + php8Parser: @php8PhpParser scanFiles: %scanFiles% scanDirectories: %scanDirectories% analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% analysedPathsFromConfig: %analysedPathsFromConfig% + playgroundMode: %sourceLocatorPlaygroundMode% - implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory + arguments: + universalObjectCratesClasses: %universalObjectCratesClasses% - - class: Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber + class: PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory arguments: - phpParser: @phpParserDecorator + phpParser: @php8PhpParser + + - + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory::create() autowired: - - Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber + - PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber - - class: Roave\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory::create() autowired: - - Roave\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + - 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\Php8 + arguments: + lexer: @php8Lexer + autowired: false + + php8Parser: + class: PHPStan\Parser\SimpleParser + arguments: + parser: @php8PhpParser + autowired: false + + pathRoutingParser: + class: PHPStan\Parser\PathRoutingParser + arguments: + currentPhpVersionRichParser: @currentPhpVersionRichParser + currentPhpVersionSimpleParser: @currentPhpVersionSimpleParser + 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.baselineNeon: - class: PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper - errorFormatter.table: class: PHPStan\Command\ErrorFormatter\TableErrorFormatter arguments: + simpleRelativePathHelper: @simpleRelativePathHelper showTipsOfTheDay: %tipsOfTheDay% + editorUrl: %editorUrl% + editorUrlTitle: %editorUrlTitle% errorFormatter.checkstyle: class: PHPStan\Command\ErrorFormatter\CheckstyleErrorFormatter @@ -1209,3 +2271,13 @@ services: class: PHPStan\Command\ErrorFormatter\GitlabErrorFormatter arguments: relativePathHelper: @simpleRelativePathHelper + + errorFormatter.github: + class: PHPStan\Command\ErrorFormatter\GithubErrorFormatter + arguments: + relativePathHelper: @simpleRelativePathHelper + + errorFormatter.teamcity: + class: PHPStan\Command\ErrorFormatter\TeamcityErrorFormatter + arguments: + relativePathHelper: @simpleRelativePathHelper diff --git a/conf/config.stubValidator.neon b/conf/config.stubValidator.neon index df76b38866..52eac72312 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -1,50 +1,37 @@ parameters: checkThisOnly: false checkClassCaseSensitivity: true - checkGenericClassInNonGenericObjectType: true - checkMissingIterableValueType: true checkMissingTypehints: true + checkMissingCallableSignature: false + __validate: false services: - class: PHPStan\PhpDoc\StubSourceLocatorFactory arguments: - parser: @phpParserDecorator - stubFiles: %stubFiles% + php8Parser: @php8PhpParser - nodeScopeResolverClassReflector: - factory: @stubClassReflector + defaultAnalysisParser!: + factory: @stubParser + + nodeScopeResolverReflector: + factory: @stubReflector stubBetterReflectionProvider: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: - classReflector: @stubClassReflector - functionReflector: @stubFunctionReflector - constantReflector: @stubConstantReflector + reflector: @stubReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false - stubClassReflector: - class: Roave\BetterReflection\Reflector\ClassReflector + stubReflector: + class: PHPStan\BetterReflection\Reflector\DefaultReflector arguments: sourceLocator: @stubSourceLocator autowired: false - stubFunctionReflector: - class: Roave\BetterReflection\Reflector\FunctionReflector - arguments: - classReflector: @stubClassReflector - sourceLocator: @stubSourceLocator - autowired: false - - stubConstantReflector: - class: Roave\BetterReflection\Reflector\ConstantReflector - arguments: - classReflector: @stubClassReflector - sourceLocator: @stubSourceLocator - autowired: false - stubSourceLocator: - class: Roave\BetterReflection\SourceLocator\Type\SourceLocator + class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator factory: @PHPStan\PhpDoc\StubSourceLocatorFactory::create() autowired: false @@ -52,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/Buffer.patch b/patches/Buffer.patch new file mode 100644 index 0000000000..1e50ecf112 --- /dev/null +++ b/patches/Buffer.patch @@ -0,0 +1,54 @@ +--- Buffer.php 2017-01-10 11:34:47.000000000 +0100 ++++ Buffer.php 2021-10-30 16:36:22.000000000 +0200 +@@ -103,7 +103,7 @@ + * + * @return \Iterator + */ +- public function getInnerIterator() ++ public function getInnerIterator(): ?\Iterator + { + return $this->_iterator; + } +@@ -133,6 +133,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function current() + { + return $this->getBuffer()->current()[self::BUFFER_VALUE]; +@@ -143,6 +144,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function key() + { + return $this->getBuffer()->current()[self::BUFFER_KEY]; +@@ -153,7 +155,7 @@ + * + * @return void + */ +- public function next() ++ public function next(): void + { + $innerIterator = $this->getInnerIterator(); + $buffer = $this->getBuffer(); +@@ -204,7 +206,7 @@ + * + * @return void + */ +- public function rewind() ++ public function rewind(): void + { + $innerIterator = $this->getInnerIterator(); + $buffer = $this->getBuffer(); +@@ -228,7 +230,7 @@ + * + * @return bool + */ +- public function valid() ++ public function valid(): bool + { + return + $this->getBuffer()->valid() && diff --git a/patches/Consistency.patch b/patches/Consistency.patch new file mode 100644 index 0000000000..4409109b36 --- /dev/null +++ b/patches/Consistency.patch @@ -0,0 +1,45 @@ +--- Consistency.php 2017-05-02 14:18:12.000000000 +0200 ++++ Consistency.php 2020-05-05 08:28:35.000000000 +0200 +@@ -319,42 +319,6 @@ + $define('STREAM_CRYPTO_METHOD_ANY_CLIENT', 63); + } + +-if (!function_exists('curry')) { +- /** +- * Curry. +- * Example: +- * $c = curry('str_replace', …, …, 'foobar'); +- * var_dump($c('foo', 'baz')); // bazbar +- * $c = curry('str_replace', 'foo', 'baz', …); +- * var_dump($c('foobarbaz')); // bazbarbaz +- * Nested curries also work: +- * $c1 = curry('str_replace', …, …, 'foobar'); +- * $c2 = curry($c1, 'foo', …); +- * var_dump($c2('baz')); // bazbar +- * Obviously, as the first argument is a callable, we can combine this with +- * \Hoa\Consistency\Xcallable ;-). +- * The “…” character is the HORIZONTAL ELLIPSIS Unicode character (Unicode: +- * 2026, UTF-8: E2 80 A6). +- * +- * @param mixed $callable Callable (two parts). +- * @param ... ... Arguments. +- * @return \Closure +- */ +- function curry($callable) +- { +- $arguments = func_get_args(); +- array_shift($arguments); +- $ii = array_keys($arguments, …, true); +- +- return function () use ($callable, $arguments, $ii) { +- return call_user_func_array( +- $callable, +- array_replace($arguments, array_combine($ii, func_get_args())) +- ); +- }; +- } +-} +- + /** + * Flex entity. + */ 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 new file mode 100644 index 0000000000..d17a378444 --- /dev/null +++ b/patches/Lookahead.patch @@ -0,0 +1,52 @@ +--- Lookahead.php 2017-01-10 11:34:47.000000000 +0100 ++++ Lookahead.php 2021-10-30 16:35:30.000000000 +0200 +@@ -93,7 +93,7 @@ + * + * @return \Iterator + */ +- public function getInnerIterator() ++ public function getInnerIterator(): ?\Iterator + { + return $this->_iterator; + } +@@ -103,6 +103,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function current() + { + return $this->_current; +@@ -113,6 +114,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function key() + { + return $this->_key; +@@ -123,6 +125,7 @@ + * + * @return void + */ ++ #[\ReturnTypeWillChange] + public function next() + { + $innerIterator = $this->getInnerIterator(); +@@ -143,6 +146,7 @@ + * + * @return void + */ ++ #[\ReturnTypeWillChange] + public function rewind() + { + $out = $this->getInnerIterator()->rewind(); +@@ -156,7 +160,7 @@ + * + * @return bool + */ +- public function valid() ++ public function valid(): bool + { + return $this->_valid; + } diff --git a/patches/Node.patch b/patches/Node.patch new file mode 100644 index 0000000000..d289251ebd --- /dev/null +++ b/patches/Node.patch @@ -0,0 +1,46 @@ +--- Node/Node.php 2017-01-14 13:26:10.000000000 +0100 ++++ Node/Node.php 2021-10-30 16:32:43.000000000 +0200 +@@ -108,7 +108,7 @@ + * @return \Hoa\Protocol\Protocol + * @throws \Hoa\Protocol\Exception + */ +- public function offsetSet($name, $node) ++ public function offsetSet($name, $node): void + { + if (!($node instanceof self)) { + throw new Protocol\Exception( +@@ -141,6 +141,7 @@ + * @return \Hoa\Protocol\Protocol + * @throws \Hoa\Protocol\Exception + */ ++ #[\ReturnTypeWillChange] + public function offsetGet($name) + { + if (!isset($this[$name])) { +@@ -160,7 +161,7 @@ + * @param string $name Node's name. + * @return bool + */ +- public function offsetExists($name) ++ public function offsetExists($name): bool + { + return true === array_key_exists($name, $this->_children); + } +@@ -171,7 +172,7 @@ + * @param string $name Node's name to remove. + * @return void + */ +- public function offsetUnset($name) ++ public function offsetUnset($name): void + { + unset($this->_children[$name]); + +@@ -365,7 +366,7 @@ + * + * @return \ArrayIterator + */ +- public function getIterator() ++ public function getIterator(): \Traversable + { + return new \ArrayIterator($this->_children); + } diff --git a/patches/PDO.patch b/patches/PDO.patch new file mode 100644 index 0000000000..607a23fda2 --- /dev/null +++ b/patches/PDO.patch @@ -0,0 +1,11 @@ +--- PDO/PDO.php 2021-12-26 15:44:39.000000000 +0100 ++++ PDO/PDO.php 2022-01-03 22:54:21.000000000 +0100 +@@ -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)
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 new file mode 100644 index 0000000000..faf698a5ff --- /dev/null +++ b/patches/Rule.patch @@ -0,0 +1,14 @@ +--- Llk/Rule/Rule.php 2017-08-08 09:44:07.000000000 +0200 ++++ Llk/Rule/Rule.php 2021-10-29 16:42:12.000000000 +0200 +@@ -118,7 +118,10 @@ + { + $this->setName($name); + $this->setChildren($children); +- $this->setNodeId($nodeId); ++ ++ if ($nodeId !== null) { ++ $this->setNodeId($nodeId); ++ } + + return; + } diff --git a/patches/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 new file mode 100644 index 0000000000..ba45ffc1b5 --- /dev/null +++ b/patches/SessionHandler.patch @@ -0,0 +1,20 @@ +--- session/SessionHandler.php 2021-11-04 14:27:30.000000000 +0100 ++++ session/SessionHandler.php 2021-11-05 11:26:14.000000000 +0100 +@@ -147,7 +147,7 @@ + *

+ */ + #[TentativeType] +- public function validateId(string $id): bool; ++ public function validateId($id): bool; + + /** + * Update timestamp of a session +@@ -163,7 +163,7 @@ + * @return bool + */ + #[TentativeType] +- public function updateTimestamp(string $id, string $data): bool; ++ public function updateTimestamp($id, $data): bool; + } + + /** diff --git a/patches/Stream.patch b/patches/Stream.patch new file mode 100644 index 0000000000..8dbb2e108e --- /dev/null +++ b/patches/Stream.patch @@ -0,0 +1,32 @@ +--- 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 + */ +- final private static function &_getStream( ++ private static function &_getStream( + $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'); + + /** +- * Shutdown method. +- */ +-Consistency::registerShutdownFunction(xcallable('Hoa\Stream\Stream::_Hoa_Stream')); +- +-/** + * Add the `hoa://Library/Stream` node. Should be use to reach/get an entry + * in the stream register. + */ 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 new file mode 100644 index 0000000000..b8282376bd --- /dev/null +++ b/patches/Wrapper.patch @@ -0,0 +1,27 @@ +--- Wrapper.php 2017-01-14 13:26:10.000000000 +0100 ++++ Wrapper.php 2020-05-05 08:39:18.000000000 +0200 +@@ -582,24 +582,3 @@ + stream_wrapper_register('hoa', Wrapper::class); + + } +- +-namespace +-{ +- +-/** +- * Alias of `Hoa\Protocol::resolve` method. +- * +- * @param string $path Path to resolve. +- * @param bool $exists If `true`, try to find the first that exists, +- * else return the first solution. +- * @param bool $unfold Return all solutions instead of one. +- * @return mixed +- */ +-if (!function_exists('resolve')) { +- function resolve($path, $exists = true, $unfold = false) +- { +- return Hoa\Protocol::getInstance()->resolve($path, $exists, $unfold); +- } +-} +- +-} 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 64222d1751..fa9198745f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,63 +1,120 @@ - + + + + + + + + + bin + src + tests + compiler/src + compiler/tests + apigen/src + changelog-generator/src + changelog-generator/run.php + issue-bot/src + issue-bot/console.php + + - + - + - - + + + + - + + - + - + + + src/Rules/Whitespace/FileWhitespaceRule.php + + + 10 + - + + - + + + 10 + + - + - + + + 10 src/Command/CommandHelper.php + src/Testing/PHPStanTestCase.php tests - - src/Command/AnalyseApplication.php - + + + + + - - + + + + + + + + + + + + + + + + + + + + + @@ -71,30 +128,94 @@ - - - - + - - - + + + + + + + + + + + + + src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - tests/*/data + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 bc4ee43f14..035be525fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,247 +1,1987 @@ parameters: ignoreErrors: - - message: "#^Strict comparison using \\=\\=\\= between PhpParser\\\\Node\\\\Expr\\\\ArrayItem and null will always evaluate to false\\.$#" + 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: "#^Only numeric types are allowed in pre\\-decrement, bool\\|float\\|int\\|string\\|null 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: "#^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: 2 + path: src/Analyser/NodeScopeResolver.php + + - + message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#' + identifier: argument.type + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + 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 \\$headers \\(array\\\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$headers \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\StyleInterface\\:\\:table\\(\\)$#" + 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\.$#' + identifier: property.onlyWritten + count: 1 + path: src/Command/CommandHelper.php + + - + 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: "#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\\\DI\\\\Config\\\\Helpers\\.$#" + 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/DependencyInjection/ContainerFactory.php + + - + message: '#^Variable method call on Nette\\Schema\\Elements\\AnyOf\|Nette\\Schema\\Elements\\Structure\|Nette\\Schema\\Elements\\Type\.$#' + identifier: method.dynamicName + count: 1 + path: src/DependencyInjection/ContainerFactory.php + + - + message: '#^Variable static method call on Nette\\Schema\\Expect\.$#' + identifier: staticMethod.dynamicName + count: 1 + path: src/DependencyInjection/ContainerFactory.php + + - + message: '#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\DI\\Config\\Helpers\.$#' + identifier: classConstant.deprecatedClass count: 1 path: src/DependencyInjection/NeonAdapter.php - - message: "#^Variable static method call on Nette\\\\Schema\\\\Expect\\.$#" + 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/ParametersSchemaExtension.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 method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#" - count: 2 - path: src/PhpDoc/PhpDocBlock.php + 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/Parser/RichParser.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: "#^Variable static method call on PHPStan\\\\PhpDoc\\\\PhpDocBlock\\.$#" + 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/PhpDocBlock.php + path: src/PhpDoc/PhpDocNodeResolver.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: '#^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/Tag/ParamTag.php + path: src/PhpDoc/PhpDocNodeResolver.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: '#^Method PHPStan\\PhpDoc\\ResolvedPhpDocBlock\:\:getNameScope\(\) should return PHPStan\\Analyser\\NameScope but returns PHPStan\\Analyser\\NameScope\|null\.$#' + identifier: return.type count: 1 - path: src/PhpDoc/Tag/ReturnTag.php + path: src/PhpDoc/ResolvedPhpDocBlock.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\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/PhpDoc/Tag/VarTag.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Only booleans are allowed in a negated boolean, int\\|false given\\.$#" + 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/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 4 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + 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: "#^Parameter \\#1 \\$str of function substr expects string, string\\|null given\\.$#" + message: '#^Dead catch \- PHPStan\\BetterReflection\\Identifier\\Exception\\InvalidIdentifierName is never thrown in the try block\.$#' + identifier: catch.neverThrown count: 3 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Parameter \\#1 \\$haystack of function strrpos expects string, string\\|null given\\.$#" + message: '#^Dead catch \- PHPStan\\BetterReflection\\NodeCompiler\\Exception\\UnableToCompileNode is never thrown in the try block\.$#' + identifier: catch.neverThrown count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/BetterReflectionProvider.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/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#" + 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/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Parameter \\#2 \\$subject of function preg_match_all expects string, string\\|null given\\.$#" + message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#' + identifier: argument.type count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php + + - + 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/FileReadTrapStreamWrapper.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\\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: "#^Property PHPStan\\\\Reflection\\\\ClassReflection\\:\\:\\$reflection with generic class ReflectionClass does not specify its types\\: T$#" + 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: '#^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/ReflectionClassSourceLocator.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/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/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php + + - + 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: '#^Doing instanceof PHPStan\\Type\\Generic\\GenericObjectType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 3 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\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" + 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/Php/BuiltinMethodReflection.php + path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Property PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:\\$declaringClass 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/Php/FakeBuiltinMethodReflection.php + path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:__construct\\(\\) has parameter \\$declaringClass with generic class ReflectionClass but 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/Php/FakeBuiltinMethodReflection.php + path: src/Reflection/InitializerExprTypeResolver.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\\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\\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: 1 + path: src/Rules/Classes/RequireImplementsRule.php + + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' + 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/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: 1 + 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/Rules/Generics/GenericAncestorsCheck.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/Generics/TemplateTypeCheck.php + + - + message: '#^Function class_implements\(\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\. Use objects retrieved from ReflectionProvider instead\.$#' + identifier: phpstanApi.runtimeReflection + count: 1 + path: src/Rules/LazyRegistry.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/LazyRegistry.php + + - + 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/Rules/LazyRegistry.php + + - + 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/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: '#^Property PHPStan\\Rules\\LazyRegistry\:\:\$rules with generic interface PHPStan\\Rules\\Rule does not specify its types\: TNodeType$#' + identifier: missingType.generics 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: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/PhpClassReflectionExtension.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\PhpClassReflectionExtension\\:\\:collectTraits\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Reflection/Php/PhpClassReflectionExtension.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Property PHPStan\\\\Rules\\\\Registry\\:\\:\\$rules with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\IterableType is error\-prone and deprecated\. Use Type\:\:isIterable\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: src/Rules/Registry.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: "#^Property PHPStan\\\\Rules\\\\Registry\\:\\:\\$cache with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + 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/Registry.php + path: src/Rules/Methods/StaticMethodCallCheck.php - - message: "#^Method PHPStan\\\\Rules\\\\Registry\\:\\:__construct\\(\\) has parameter \\$rules with generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + 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: "#^Cannot call method getActiveTemplateTypeMap\\(\\) on PHPStan\\\\Reflection\\\\ClassReflection\\|null\\.$#" + message: '#^Access to an undefined property T of PHPStan\\Rules\\RuleError\:\:\$tip\.$#' + identifier: property.notFound count: 2 - path: src/Type/Generic/GenericObjectType.php + path: src/Rules/RuleErrorBuilder.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/Rules/RuleLevelHelper.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: ''' + #^Call to deprecated method assertFileNotExists\(\) of class PHPUnit\\Framework\\Assert\: + https\://github\.com/sebastianbergmann/phpunit/issues/4077$# + ''' + identifier: staticMethod.deprecated count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRule.php + path: src/Testing/LevelsTestCase.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Call to function method_exists\(\) with ''PHPUnit\\\\Framework\\\\TestCase'' and ''assertFileDoesNotEx…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Testing/LevelsTestCase.php + + - + message: '#^Catching internal class PHPUnit\\Framework\\AssertionFailedError\.$#' + identifier: catch.internalClass + count: 2 + path: src/Testing/LevelsTestCase.php - - message: "#^Method PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest\\:\\:getRule\\(\\) return type 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: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Testing/LevelsTestCase.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: '#^Anonymous function has an unused use \$container\.$#' + identifier: closure.unusedUse count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderRule.php + path: src/Testing/PHPStanTestCase.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: '#^Catching internal class PHPUnit\\Framework\\ExpectationFailedException\.$#' + identifier: catch.internalClass count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderTest.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: "#^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/AccessoryArrayListType.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/AccessoryLiteralStringType.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/AccessoryLowercaseStringType.php - - message: "#^Method PHPStan\\\\Node\\\\FileNodeTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Type/Accessory/AccessoryNonEmptyStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryNonEmptyStringType.php + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryNonFalsyStringType.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/Accessory/AccessoryNumericStringType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + 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: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/HasMethodType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/HasOffsetType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#' + 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: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/HasOffsetValueType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/HasPropertyType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/NonEmptyArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/OversizedArrayType.php + + - + message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' + 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: '#^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: 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: '#^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/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: '#^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: src/Type/ClosureType.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/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 @@ + + + + + src + + + + + + + + + + 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/src/Reflection/SignatureMap/functionMap.php b/resources/functionMap.php similarity index 81% rename from src/Reflection/SignatureMap/functionMap.php rename to resources/functionMap.php index 84fdf667b3..aea642904a 100644 --- a/src/Reflection/SignatureMap/functionMap.php +++ b/resources/functionMap.php @@ -57,10 +57,7 @@ return [ '_' => ['string', 'message'=>'string'], -'__halt_compiler' => ['void'], -'abs' => ['int', 'number'=>'int'], -'abs\'1' => ['float', 'number'=>'float'], -'abs\'2' => ['float|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'], @@ -70,110 +67,110 @@ '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'], 'apache_getenv' => ['string|false', 'variable'=>'string', 'walk_to_top='=>'bool'], -'apache_lookup_uri' => ['object', 'filename'=>'string'], +'apache_lookup_uri' => ['object|false', 'filename'=>'string'], 'apache_note' => ['string|false', 'note_name'=>'string', 'note_value='=>'string'], 'apache_request_headers' => ['array|false'], 'apache_reset_timeout' => ['bool'], @@ -212,29 +209,29 @@ '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' => ['bool', 'key'=>'string|APCuIterator'], +'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'], -'APCUIterator::current' => ['mixed'], -'APCUIterator::getTotalCount' => ['int'], -'APCUIterator::getTotalHits' => ['int'], -'APCUIterator::getTotalSize' => ['int'], -'APCUIterator::key' => ['string'], -'APCUIterator::next' => ['void'], -'APCUIterator::rewind' => ['void'], -'APCUIterator::valid' => ['bool'], +'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], +'APCuIterator::current' => ['mixed'], +'APCuIterator::getTotalCount' => ['int'], +'APCuIterator::getTotalHits' => ['int'], +'APCuIterator::getTotalSize' => ['int'], +'APCuIterator::key' => ['string'], +'APCuIterator::next' => ['void'], +'APCuIterator::rewind' => ['void'], +'APCuIterator::valid' => ['bool'], 'apd_breakpoint' => ['bool', 'debug_level'=>'int'], 'apd_callstack' => ['array'], 'apd_clunk' => ['void', 'warning'=>'string', 'delimiter='=>'string'], @@ -250,7 +247,7 @@ 'apd_set_session_trace' => ['void', 'debug_level'=>'int', 'dump_directory='=>'string'], 'apd_set_session_trace_socket' => ['bool', 'tcp_server'=>'string', 'socket_type'=>'int', 'port'=>'int', 'debug_level'=>'int'], 'AppendIterator::__construct' => ['void'], -'AppendIterator::append' => ['void', 'it'=>'iterator'], +'AppendIterator::append' => ['void', 'iterator'=>'Iterator'], 'AppendIterator::current' => ['mixed'], 'AppendIterator::getArrayIterator' => ['ArrayIterator'], 'AppendIterator::getInnerIterator' => ['iterator'], @@ -260,10 +257,10 @@ 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], 'array_change_key_case' => ['array', 'input'=>'array', 'case='=>'int'], -'array_chunk' => ['array[]', 'input'=>'array', 'size'=>'int', 'preserve_keys='=>'bool'], +'array_chunk' => ['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' => ['int[]', 'input'=>'array'], +'array_count_values' => ['array', 'input'=>'array'], 'array_diff' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], 'array_diff_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], 'array_diff_key' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], @@ -274,7 +271,7 @@ 'array_fill' => ['array', 'start_key'=>'int', 'num'=>'int', 'val'=>'mixed'], 'array_fill_keys' => ['array', 'keys'=>'array', 'val'=>'mixed'], 'array_filter' => ['array', 'input'=>'array', 'callback='=>'callable(mixed,mixed):bool|callable(mixed):bool', 'flag='=>'int'], -'array_flip' => ['array', 'input'=>'array'], +'array_flip' => ['array', 'input'=>'array'], 'array_intersect' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], 'array_intersect_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], 'array_intersect_key' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], @@ -283,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_map' => ['array', 'callback'=>'?callable', 'input1'=>'array', '...args='=>'array'], +'array_key_first' => ['int|string|null', 'array'=>'array'], +'array_key_last' => ['int|string|null', 'array'=>'array'], +'array_keys' => ['list', 'input'=>'array', 'search_value='=>'mixed', 'strict='=>'bool'], +'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...args='=>'array'], 'array_merge' => ['array', 'arr1'=>'array', '...args='=>'array'], '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'], @@ -303,7 +300,7 @@ 'array_search' => ['int|string|false', 'needle'=>'mixed', 'haystack'=>'array', 'strict='=>'bool'], 'array_shift' => ['mixed', '&rw_stack'=>'array'], 'array_slice' => ['array', 'input'=>'array', 'offset'=>'int', 'length='=>'?int', 'preserve_keys='=>'bool'], -'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'array|string'], +'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'mixed'], 'array_sum' => ['int|float', 'input'=>'array'], 'array_udiff' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'], 'array_udiff\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], @@ -317,11 +314,11 @@ 'array_uintersect_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_uintersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'callable(mixed,mixed):int', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], -'array_unique' => ['array', 'input'=>'array', 'sort_flags='=>'int'], -'array_unshift' => ['int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], -'array_values' => ['array', 'input'=>'array'], -'array_walk' => ['bool', '&rw_input'=>'array', 'callback'=>'callable', 'userdata='=>'mixed'], -'array_walk_recursive' => ['bool', '&rw_input'=>'array', 'callback'=>'callable', 'userdata='=>'mixed'], +'array_unique' => ['array', 'array'=>'array', 'flags='=>'int'], +'array_unshift' => ['positive-int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], +'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'], 'ArrayAccess::offsetGet' => ['mixed', 'offset'=>'mixed'], 'ArrayAccess::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], @@ -329,7 +326,7 @@ 'ArrayIterator::__construct' => ['void', 'array='=>'array|object', 'flags='=>'int'], 'ArrayIterator::append' => ['void', 'value'=>'mixed'], 'ArrayIterator::asort' => ['void'], -'ArrayIterator::count' => ['int'], +'ArrayIterator::count' => ['0|positive-int'], 'ArrayIterator::current' => ['mixed'], 'ArrayIterator::getArrayCopy' => ['array'], 'ArrayIterator::getFlags' => ['int'], @@ -346,14 +343,14 @@ 'ArrayIterator::seek' => ['void', 'position'=>'int'], 'ArrayIterator::serialize' => ['string'], 'ArrayIterator::setFlags' => ['void', 'flags'=>'string'], -'ArrayIterator::uasort' => ['void', 'cmp_function'=>'callable(mixed,mixed):int'], -'ArrayIterator::uksort' => ['void', 'cmp_function'=>'callable(mixed,mixed):int'], +'ArrayIterator::uasort' => ['void', 'callback'=>'callable(mixed,mixed):int'], +'ArrayIterator::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'ArrayIterator::unserialize' => ['void', 'serialized'=>'string'], 'ArrayIterator::valid' => ['bool'], -'ArrayObject::__construct' => ['void', 'input='=>'array|object', 'flags='=>'int', 'iterator_class='=>'string'], +'ArrayObject::__construct' => ['void', 'input='=>'array|object', 'flags='=>'int', 'iterator_class='=>'class-string'], 'ArrayObject::append' => ['void', 'value'=>'mixed'], 'ArrayObject::asort' => ['void'], -'ArrayObject::count' => ['int'], +'ArrayObject::count' => ['0|positive-int'], 'ArrayObject::exchangeArray' => ['array', 'ar'=>'mixed'], 'ArrayObject::getArrayCopy' => ['array'], 'ArrayObject::getFlags' => ['int'], @@ -369,8 +366,8 @@ 'ArrayObject::serialize' => ['string'], 'ArrayObject::setFlags' => ['void', 'flags'=>'int'], 'ArrayObject::setIteratorClass' => ['void', 'iterator_class'=>'string'], -'ArrayObject::uasort' => ['void', 'cmp_function'=>'callable'], -'ArrayObject::uksort' => ['void', 'cmp_function'=>'callable'], +'ArrayObject::uasort' => ['void', 'callback'=>'callable'], +'ArrayObject::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'ArrayObject::unserialize' => ['void', 'serialized'=>'string'], 'arsort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], 'asin' => ['float', 'number'=>'float'], @@ -396,7 +393,7 @@ 'BadFunctionCallException::getLine' => ['int'], 'BadFunctionCallException::getMessage' => ['string'], 'BadFunctionCallException::getPrevious' => ['(?Throwable)|(?BadFunctionCallException)'], -'BadFunctionCallException::getTrace' => ['array'], +'BadFunctionCallException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'BadFunctionCallException::getTraceAsString' => ['string'], 'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?BadMethodCallException)'], @@ -406,9 +403,10 @@ 'BadMethodCallException::getLine' => ['int'], 'BadMethodCallException::getMessage' => ['string'], 'BadMethodCallException::getPrevious' => ['(?Throwable)|(?BadMethodCallException)'], -'BadMethodCallException::getTrace' => ['array'], +'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'], @@ -419,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' => ['string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bccomp' => ['int', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcdiv' => ['string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcmod' => ['string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'], -'bcmul' => ['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'], @@ -437,15 +435,15 @@ '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' => ['string', 'base'=>'string', 'exponent'=>'string', 'scale='=>'int'], -'bcpowmod' => ['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' => ['string', 'operand'=>'string', 'scale='=>'int'], -'bcsub' => ['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', 'domain'=>'string', 'codeset'=>'string'], -'bindec' => ['int', 'binary_number'=>'string'], -'bindtextdomain' => ['string', 'domain_name'=>'string', 'dir'=>'string'], +'bind_textdomain_codeset' => ['string|false', 'domain'=>'string', 'codeset'=>'string'], +'bindec' => ['float|int', 'binary_number'=>'string'], +'bindtextdomain' => ['string|false', 'domain_name'=>'string', 'dir'=>'string'], 'birdstep_autocommit' => ['bool', 'index'=>'int'], 'birdstep_close' => ['bool', 'id'=>'int'], 'birdstep_commit' => ['bool', 'index'=>'int'], @@ -483,18 +481,18 @@ 'bson_decode' => ['array', 'bson'=>'string'], 'bson_encode' => ['string', 'anything'=>'mixed'], 'bzclose' => ['bool', 'bz'=>'resource'], -'bzcompress' => ['string', 'source'=>'string', 'blocksize100k='=>'int', 'workfactor='=>'int'], -'bzdecompress' => ['string', 'source'=>'string', 'small='=>'int'], +'bzcompress' => ['string|int', 'source'=>'string', 'blocksize100k='=>'int', 'workfactor='=>'int'], +'bzdecompress' => ['string|false', 'source'=>'string', 'small='=>'int'], 'bzerrno' => ['int', 'bz'=>'resource'], 'bzerror' => ['array', 'bz'=>'resource'], 'bzerrstr' => ['string', 'bz'=>'resource'], 'bzflush' => ['bool', 'bz'=>'resource'], -'bzopen' => ['resource', 'file'=>'string|resource', 'mode'=>'string'], -'bzread' => ['string', 'bz'=>'resource', 'length='=>'int'], -'bzwrite' => ['int', 'bz'=>'resource', 'data'=>'string', 'length='=>'int'], -'CachingIterator::__construct' => ['void', 'it'=>'iterator', 'flags='=>''], +'bzopen' => ['resource|false', 'file'=>'string|resource', 'mode'=>'string'], +'bzread' => ['string|false', 'bz'=>'resource', 'length='=>'int'], +'bzwrite' => ['int|false', 'bz'=>'resource', 'data'=>'string', 'length='=>'int'], +'CachingIterator::__construct' => ['void', 'iterator'=>'Iterator', 'flags='=>''], 'CachingIterator::__toString' => ['string'], -'CachingIterator::count' => ['int'], +'CachingIterator::count' => ['0|positive-int'], 'CachingIterator::current' => ['mixed'], 'CachingIterator::getCache' => ['array'], 'CachingIterator::getFlags' => ['int'], @@ -925,15 +923,15 @@ 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'call_user_method' => ['mixed', 'method_name'=>'string', 'obj'=>'object', 'parameter='=>'mixed', '...args='=>'mixed'], 'call_user_method_array' => ['mixed', 'method_name'=>'string', 'obj'=>'object', 'params'=>'array'], -'CallbackFilterIterator::__construct' => ['void', 'it'=>'iterator', 'func'=>'callable'], +'CallbackFilterIterator::__construct' => ['void', 'iterator'=>'Iterator', 'func'=>'callable'], 'CallbackFilterIterator::accept' => ['bool'], 'CallbackFilterIterator::current' => ['mixed'], -'CallbackFilterIterator::getInnerIterator' => ['iterator'], +'CallbackFilterIterator::getInnerIterator' => ['Iterator'], 'CallbackFilterIterator::key' => ['mixed'], 'CallbackFilterIterator::next' => ['void'], 'CallbackFilterIterator::rewind' => ['void'], 'CallbackFilterIterator::valid' => ['bool'], -'ceil' => ['float|int', 'number'=>'float'], +'ceil' => ['__benevolent', 'number'=>'float'], 'chdb::__construct' => ['void', 'pathname'=>'string'], 'chdb::get' => ['string', 'key'=>'string'], 'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'], @@ -944,14 +942,14 @@ 'chmod' => ['bool', 'filename'=>'string', 'mode'=>'int'], 'chop' => ['string', 'str'=>'string', 'character_mask='=>'string'], 'chown' => ['bool', 'filename'=>'string', 'user'=>'string|int'], -'chr' => ['string', 'ascii'=>'int'], +'chr' => ['non-empty-string', 'ascii'=>'int'], 'chroot' => ['bool', 'directory'=>'string'], -'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'int', 'ending='=>'string'], +'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'positive-int', 'ending='=>'string'], 'class_alias' => ['bool', 'user_class_name'=>'string', 'alias_name'=>'string', 'autoload='=>'bool'], 'class_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], -'class_implements' => ['array', 'what'=>'object|string', 'autoload='=>'bool'], -'class_parents' => ['array', 'instance'=>'object|string', 'autoload='=>'bool'], -'class_uses' => ['array', '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'], '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'], @@ -990,14 +988,14 @@ 'ClosedGeneratorException::getLine' => ['int'], 'ClosedGeneratorException::getMessage' => ['string'], 'ClosedGeneratorException::getPrevious' => ['Throwable|ClosedGeneratorException|null'], -'ClosedGeneratorException::getTrace' => ['array'], +'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,8 +1005,8 @@ 'clusterObj::setGroup' => ['int', 'expression'=>'string'], 'Collator::__construct' => ['void', 'locale'=>'string'], 'Collator::asort' => ['bool', '&rw_arr'=>'array', 'sort_flag='=>'int'], -'Collator::compare' => ['int', 'str1'=>'string', 'str2'=>'string'], -'Collator::create' => ['Collator', 'locale'=>'string'], +'Collator::compare' => ['int|false', 'str1'=>'string', 'str2'=>'string'], +'Collator::create' => ['?Collator', 'locale'=>'string'], 'Collator::getAttribute' => ['int', 'attr'=>'int'], 'Collator::getErrorCode' => ['int'], 'Collator::getErrorMessage' => ['string'], @@ -1020,13 +1018,13 @@ 'Collator::sort' => ['bool', '&rw_arr'=>'array', 'sort_flags='=>'int'], 'Collator::sortWithSortKeys' => ['bool', '&rw_arr'=>'array'], 'collator_asort' => ['bool', 'coll'=>'collator', '&rw_arr'=>'array', 'sort_flag='=>'int'], -'collator_compare' => ['int', 'coll'=>'collator', 'str1'=>'string', 'str2'=>'string'], -'collator_create' => ['Collator', 'locale'=>'string'], -'collator_get_attribute' => ['int', 'coll'=>'collator', 'attr'=>'int'], -'collator_get_error_code' => ['int', 'coll'=>'collator'], -'collator_get_error_message' => ['string', 'coll'=>'collator'], -'collator_get_locale' => ['string', 'coll'=>'collator', 'type'=>'int'], -'collator_get_sort_key' => ['string', 'coll'=>'collator', 'str'=>'string'], +'collator_compare' => ['int|false', 'coll'=>'collator', 'str1'=>'string', 'str2'=>'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'], +'collator_get_locale' => ['string|false', 'coll'=>'collator', 'type'=>'int'], +'collator_get_sort_key' => ['string|false', 'coll'=>'collator', 'str'=>'string'], 'collator_get_strength' => ['int', 'coll'=>'collator'], 'collator_set_attribute' => ['bool', 'coll'=>'collator', 'attr'=>'int', 'val'=>'int'], 'collator_set_strength' => ['bool', 'coll'=>'collator', 'strength'=>'int'], @@ -1040,7 +1038,7 @@ 'COM::__get' => ['', 'name'=>''], 'COM::__set' => ['', 'name'=>'', 'value'=>''], 'com_addref' => [''], -'com_create_guid' => ['string'], +'com_create_guid' => ['string|false'], 'com_event_sink' => ['bool', 'comobject'=>'object', 'sinkobject'=>'object', 'sinkinterface='=>'mixed'], 'com_get_active_object' => ['object', 'progid'=>'string', 'code_page='=>'int'], 'com_isenum' => ['bool', 'com_module'=>'variant'], @@ -1048,7 +1046,7 @@ 'com_message_pump' => ['bool', 'timeoutms='=>'int'], 'com_print_typeinfo' => ['bool', 'comobject_or_typelib'=>'object', 'dispinterface='=>'string', 'wantsink='=>'bool'], 'com_release' => [''], -'compact' => ['array', '...var_names='=>'string|array'], +'compact' => ['array', '...var_names='=>'string|array'], 'COMPersistHelper::__construct' => ['void', 'com_object'=>'object'], 'COMPersistHelper::GetCurFile' => ['string'], 'COMPersistHelper::GetMaxStreamSize' => ['int'], @@ -1060,12 +1058,12 @@ 'componere\cast' => ['Type', 'arg1'=>'', 'object'=>''], 'componere\cast_by_ref' => ['Type', 'arg1'=>'', 'object'=>''], 'confirm_pdo_ibm_compiled' => [''], -'connection_aborted' => ['int'], -'connection_status' => ['int'], +'connection_aborted' => ['0|1'], +'connection_status' => ['int-mask'], 'connection_timeout' => ['int'], 'constant' => ['mixed', 'const_name'=>'string'], 'convert_cyr_string' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], -'convert_uudecode' => ['string', 'data'=>'string'], +'convert_uudecode' => ['string|false', 'data'=>'string'], 'convert_uuencode' => ['string', 'data'=>'string'], 'copy' => ['bool', 'source_file'=>'string', 'destination_file'=>'string', 'context='=>'resource'], 'cos' => ['float', 'number'=>'float'], @@ -1365,9 +1363,9 @@ 'Couchbase\WildcardSearchQuery::jsonSerialize' => ['array'], 'Couchbase\zlibCompress' => ['string', 'data'=>'string'], 'Couchbase\zlibDecompress' => ['string', 'data'=>'string'], -'count' => ['int', 'var'=>'Countable|array', 'mode='=>'int'], -'count_chars' => ['mixed', 'input'=>'string', 'mode='=>'int'], -'Countable::count' => ['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'], 'crack_closedict' => ['bool', 'dictionary='=>'resource'], 'crack_getlastmessage' => ['string'], @@ -1375,18 +1373,18 @@ 'crash' => [''], 'crc32' => ['int', 'str'=>'string'], 'create_function' => ['string', 'args'=>'string', 'code'=>'string'], -'crypt' => ['string', 'str'=>'string', 'salt='=>'string'], -'ctype_alnum' => ['bool', 'c'=>'string|int'], -'ctype_alpha' => ['bool', 'c'=>'string|int'], -'ctype_cntrl' => ['bool', 'c'=>'string|int'], -'ctype_digit' => ['bool', 'c'=>'string|int'], -'ctype_graph' => ['bool', 'c'=>'string|int'], -'ctype_lower' => ['bool', 'c'=>'string|int'], -'ctype_print' => ['bool', 'c'=>'string|int'], -'ctype_punct' => ['bool', 'c'=>'string|int'], -'ctype_space' => ['bool', 'c'=>'string|int'], -'ctype_upper' => ['bool', 'c'=>'string|int'], -'ctype_xdigit' => ['bool', 'c'=>'string|int'], +'crypt' => ['non-empty-string', 'str'=>'string', 'salt='=>'string'], +'ctype_alnum' => ['bool', 'c'=>'mixed'], +'ctype_alpha' => ['bool', 'c'=>'mixed'], +'ctype_cntrl' => ['bool', 'c'=>'mixed'], +'ctype_digit' => ['bool', 'c'=>'mixed'], +'ctype_graph' => ['bool', 'c'=>'mixed'], +'ctype_lower' => ['bool', 'c'=>'mixed'], +'ctype_print' => ['bool', 'c'=>'mixed'], +'ctype_punct' => ['bool', 'c'=>'mixed'], +'ctype_space' => ['bool', 'c'=>'mixed'], +'ctype_upper' => ['bool', 'c'=>'mixed'], +'ctype_xdigit' => ['bool', 'c'=>'mixed'], 'cubrid_affected_rows' => ['int', 'req_identifier='=>''], 'cubrid_bind' => ['bool', 'req_identifier'=>'resource', 'bind_param'=>'int', 'bind_value'=>'mixed', 'bind_value_type='=>'string'], 'cubrid_client_encoding' => ['string', 'conn_identifier='=>''], @@ -1487,21 +1485,21 @@ 'cubrid_unbuffered_query' => ['resource', 'query'=>'string', 'conn_identifier='=>''], 'cubrid_version' => ['string'], 'curl_close' => ['void', 'ch'=>'resource'], -'curl_copy_handle' => ['resource', 'ch'=>'resource'], +'curl_copy_handle' => ['resource|false', 'ch'=>'resource'], 'curl_errno' => ['int', 'ch'=>'resource'], 'curl_error' => ['string', 'ch'=>'resource'], -'curl_escape' => ['string', 'ch'=>'resource', 'str'=>'string'], +'curl_escape' => ['string|false', 'ch'=>'resource', 'str'=>'string'], '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'], @@ -1516,8 +1514,8 @@ 'curl_share_setopt' => ['bool', 'sh'=>'resource', 'option'=>'int', 'value'=>'mixed'], 'curl_share_strerror' => ['string', 'code'=>'int'], 'curl_strerror' => ['string', 'code'=>'int'], -'curl_unescape' => ['string', 'ch'=>'resource', 'str'=>'string'], -'curl_version' => ['array', 'version='=>'int'], +'curl_unescape' => ['string|false', 'ch'=>'resource', 'str'=>'string'], +'curl_version' => ['array|false', 'version='=>'int'], 'CURLFile::__construct' => ['void', 'filename'=>'string', 'mimetype='=>'string', 'postfilename='=>'string'], 'CURLFile::__wakeup' => ['void'], 'CURLFile::getFilename' => ['string'], @@ -1525,7 +1523,7 @@ 'CURLFile::getPostFilename' => ['string'], 'CURLFile::setMimeType' => ['void', 'mime'=>'string'], 'CURLFile::setPostFilename' => ['void', 'name'=>'string'], -'current' => ['mixed', 'array_arg'=>'array'], +'current' => ['mixed', 'array_arg'=>'array|object'], 'cyrus_authenticate' => ['void', 'connection'=>'resource', 'mechlist='=>'string', 'service='=>'string', 'user='=>'string', 'minssf='=>'int', 'maxssf='=>'int', 'authname='=>'string', 'password='=>'string'], 'cyrus_bind' => ['bool', 'connection'=>'resource', 'callbacks'=>'array'], 'cyrus_close' => ['bool', 'connection'=>'resource'], @@ -1534,64 +1532,64 @@ '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}'], -'date_interval_create_from_date_string' => ['DateInterval', 'time'=>'string'], +'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'], 'date_modify' => ['DateTime|false', 'object'=>'DateTime', 'modify'=>'string'], 'date_offset_get' => ['int', 'obj'=>'DateTimeInterface'], -'date_parse' => ['array|false', 'date'=>'string'], -'date_parse_from_format' => ['array', 'format'=>'string', 'date'=>'string'], +'date_parse' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'date'=>'string'], +'date_parse_from_format' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'format'=>'string', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], -'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], +'date_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'=>''], +'date_time_set' => ['DateTime|false', 'object'=>'', 'hour'=>'', 'minute'=>'', 'second='=>'', 'microseconds='=>''], 'date_timestamp_get' => ['int', 'obj'=>'DateTimeInterface'], 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'unixtimestamp'=>'int'], -'date_timezone_get' => ['DateTimeZone', 'obj'=>'DateTimeInterface'], +'date_timezone_get' => ['DateTimeZone|false', 'obj'=>'DateTimeInterface'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], 'datefmt_create' => ['IntlDateFormatter|false', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'string|DateTimeZone|IntlTimeZone|null', 'calendar='=>'int|IntlCalendar|null', 'pattern='=>'string'], -'datefmt_format' => ['string', 'fmt'=>'IntlDateFormatter', 'value'=>'DateTime|IntlCalendar|array|int'], -'datefmt_format_object' => ['string', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], -'datefmt_get_calendar' => ['int', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_calendar_object' => ['IntlCalendar', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_datetype' => ['int', 'fmt'=>'IntlDateFormatter'], +'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|null', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_datetype' => ['int|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_locale' => ['string', 'fmt'=>'IntlDateFormatter', 'which='=>'int'], -'datefmt_get_pattern' => ['string', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_timetype' => ['int', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_timezone' => ['IntlTimeZone'], -'datefmt_get_timezone_id' => ['string', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_locale' => ['string|false', 'fmt'=>'IntlDateFormatter', 'which='=>'int'], +'datefmt_get_pattern' => ['string|false', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_timetype' => ['int|false', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_timezone' => ['IntlTimeZone|false'], +'datefmt_get_timezone_id' => ['string|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_is_lenient' => ['bool', 'fmt'=>'IntlDateFormatter'], -'datefmt_localtime' => ['array|bool', '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'], 'DatePeriod::__construct\'2' => ['void', 'iso'=>'string', 'options='=>'int'], 'DatePeriod::__wakeup' => ['void'], 'DatePeriod::getDateInterval' => ['DateInterval'], -'DatePeriod::getEndDate' => ['DateTimeInterface'], +'DatePeriod::getEndDate' => ['?DateTimeInterface'], 'DatePeriod::getStartDate' => ['DateTimeInterface'], 'DateTime::__construct' => ['void', 'time='=>'string', 'timezone='=>'?DateTimeZone'], 'DateTime::__set_state' => ['static', 'array'=>'array'], @@ -1601,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}'], +'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'], @@ -1620,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}'], +'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'], @@ -1639,15 +1637,15 @@ 'DateTimeZone::__construct' => ['void', 'timezone'=>'string'], 'DateTimeZone::__set_state' => ['DateTimeZone', 'array'=>'array'], 'DateTimeZone::__wakeup' => ['void'], -'DateTimeZone::getLocation' => ['array'], +'DateTimeZone::getLocation' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false'], 'DateTimeZone::getName' => ['string'], 'DateTimeZone::getOffset' => ['int', 'datetime'=>'DateTimeInterface'], -'DateTimeZone::getTransitions' => ['array', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], -'DateTimeZone::listAbbreviations' => ['array'], -'DateTimeZone::listIdentifiers' => ['array', 'what='=>'int', 'country='=>'string'], -'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'], @@ -1659,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'], @@ -1675,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'], @@ -1690,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'], @@ -1717,18 +1715,18 @@ 'dba_popen' => ['resource|false', 'path'=>'string', 'mode'=>'string', 'handlername='=>'string', '...args='=>'string'], 'dba_replace' => ['bool', 'key'=>'string', 'value'=>'string', 'handle'=>'resource'], 'dba_sync' => ['bool', 'handle'=>'resource'], -'dbase_add_record' => ['bool', 'dbase_identifier'=>'int', 'record'=>'array'], -'dbase_close' => ['bool', 'dbase_identifier'=>'int'], -'dbase_create' => ['int', 'filename'=>'string', 'fields'=>'array'], -'dbase_delete_record' => ['bool', 'dbase_identifier'=>'int', 'record_number'=>'int'], -'dbase_get_header_info' => ['array', 'dbase_identifier'=>'int'], -'dbase_get_record' => ['array', 'dbase_identifier'=>'int', 'record_number'=>'int'], -'dbase_get_record_with_names' => ['array', 'dbase_identifier'=>'int', 'record_number'=>'int'], -'dbase_numfields' => ['int', 'dbase_identifier'=>'int'], -'dbase_numrecords' => ['int', 'dbase_identifier'=>'int'], -'dbase_open' => ['int', 'filename'=>'string', 'mode'=>'int'], -'dbase_pack' => ['bool', 'dbase_identifier'=>'int'], -'dbase_replace_record' => ['bool', 'dbase_identifier'=>'int', 'record'=>'array', 'record_number'=>'int'], +'dbase_add_record' => ['bool', 'dbase_identifier'=>'resource', 'record'=>'array'], +'dbase_close' => ['bool', 'dbase_identifier'=>'resource'], +'dbase_create' => ['resource|false', 'filename'=>'string', 'fields'=>'array'], +'dbase_delete_record' => ['bool', 'dbase_identifier'=>'resource', 'record_number'=>'int'], +'dbase_get_header_info' => ['array', 'dbase_identifier'=>'resource'], +'dbase_get_record' => ['array', 'dbase_identifier'=>'resource', 'record_number'=>'int'], +'dbase_get_record_with_names' => ['array', 'dbase_identifier'=>'resource', 'record_number'=>'int'], +'dbase_numfields' => ['int', 'dbase_identifier'=>'resource'], +'dbase_numrecords' => ['int', 'dbase_identifier'=>'resource'], +'dbase_open' => ['resource|false', 'filename'=>'string', 'mode'=>'int'], +'dbase_pack' => ['bool', 'dbase_identifier'=>'resource'], +'dbase_replace_record' => ['bool', 'dbase_identifier'=>'resource', 'record'=>'array', 'record_number'=>'int'], 'dbplus_add' => ['int', 'relation'=>'resource', 'tuple'=>'array'], 'dbplus_aql' => ['resource', 'query'=>'string', 'server='=>'string', 'dbpath='=>'string'], 'dbplus_chdir' => ['string', 'newdir='=>'string'], @@ -1787,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' => ['array', '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' => [''], @@ -1796,7 +1794,7 @@ 'debugger_print' => [''], 'debugger_start_debug' => [''], 'decbin' => ['string', 'decimal_number'=>'int'], -'dechex' => ['string', 'decimal_number'=>'int'], +'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'decimal_number'=>'int'], 'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], 'define_syslog_variables' => ['void'], @@ -1807,7 +1805,7 @@ 'dgettext' => ['string', 'domain_name'=>'string', 'msgid'=>'string'], 'dio_close' => ['void', 'fd'=>'resource'], 'dio_fcntl' => ['mixed', 'fd'=>'resource', 'cmd'=>'int', 'args='=>'mixed'], -'dio_open' => ['resource', 'filename'=>'string', 'flags'=>'int', 'mode='=>'int'], +'dio_open' => ['resource|false', 'filename'=>'string', 'flags'=>'int', 'mode='=>'int'], 'dio_read' => ['string', 'fd'=>'resource', 'len='=>'int'], 'dio_seek' => ['int', 'fd'=>'resource', 'pos'=>'int', 'whence='=>'int'], 'dio_stat' => ['array|null', 'fd'=>'resource'], @@ -1855,7 +1853,7 @@ 'DirectoryIterator::setFileClass' => ['void', 'class_name='=>'string'], 'DirectoryIterator::setInfoClass' => ['void', 'class_name='=>'string'], 'DirectoryIterator::valid' => ['bool'], -'dirname' => ['string', 'path'=>'string', 'levels='=>'int'], +'dirname' => ['string', 'path'=>'string', 'levels='=>'positive-int'], 'disk_free_space' => ['float|false', 'path'=>'string'], 'disk_total_space' => ['float|false', 'path'=>'string'], 'diskfreespace' => ['float|false', 'path'=>'string'], @@ -1864,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'], @@ -1884,7 +1882,7 @@ 'DomainException::getLine' => ['int'], 'DomainException::getMessage' => ['string'], 'DomainException::getPrevious' => ['Throwable|DomainException|null'], -'DomainException::getTrace' => ['array'], +'DomainException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'DomainException::getTraceAsString' => ['string'], 'DOMAttr::__construct' => ['void', 'name'=>'string', 'value='=>'string'], 'DOMAttr::isId' => ['bool'], @@ -1900,29 +1898,29 @@ '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'], 'DOMDocument::relaxNGValidateSource' => ['bool', 'source'=>'string'], -'DOMDocument::save' => ['int', 'filename'=>'string', 'options='=>'int'], +'DOMDocument::save' => ['int|false', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::saveHTML' => ['string|false', 'node='=>'?DOMNode'], 'DOMDocument::saveHTMLFile' => ['int|false', 'filename'=>'string'], 'DOMDocument::saveXML' => ['string|false', 'node='=>'?DOMNode', 'options='=>'int'], @@ -1970,7 +1968,7 @@ 'DOMImplementation::createDocument' => ['DOMDocument', 'namespaceuri='=>'string', 'qualifiedname='=>'string', 'doctype='=>'DOMDocumentType'], 'DOMImplementation::createDocumentType' => ['DOMDocumentType', 'qualifiedname='=>'string', 'publicid='=>'string', 'systemid='=>'string'], 'DOMImplementation::hasFeature' => ['bool', 'feature'=>'string', 'version'=>'string'], -'DOMNamedNodeMap::count' => ['int'], +'DOMNamedNodeMap::count' => ['0|positive-int'], 'DOMNamedNodeMap::getNamedItem' => ['?DOMNode', 'name'=>'string'], 'DOMNamedNodeMap::getNamedItemNS' => ['?DOMNode', 'namespaceuri'=>'string', 'localname'=>'string'], 'DOMNamedNodeMap::item' => ['?DOMNode', 'index'=>'int'], @@ -1988,12 +1986,12 @@ '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'], 'DOMNode::replaceChild' => ['DOMNode', 'newnode'=>'DOMNode', 'oldnode'=>'DOMNode'], -'DOMNodeList::count' => ['int'], +'DOMNodeList::count' => ['0|positive-int'], 'DOMNodeList::item' => ['?DOMNode', 'index'=>'int'], 'DOMProcessingInstruction::__construct' => ['void', 'name'=>'string', 'value'=>'string'], 'DomProcessingInstruction::data' => ['string'], @@ -2001,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'], @@ -2012,8 +2010,8 @@ 'domxml_xslt_stylesheet_file' => ['DomXsltStylesheet', 'xsl_file'=>'string'], 'domxml_xslt_version' => ['int'], 'DOMXPath::__construct' => ['void', 'doc'=>'DOMDocument'], -'DOMXPath::evaluate' => ['mixed', 'expression'=>'string', 'contextnode='=>'DOMNode', 'registernodens='=>'bool'], -'DOMXPath::query' => ['DOMNodeList|false', 'expression'=>'string', 'contextnode='=>'DOMNode', 'registernodens='=>'bool'], +'DOMXPath::evaluate' => ['mixed', 'expression'=>'string', 'contextnode='=>'?DOMNode', 'registernodens='=>'bool'], +'DOMXPath::query' => ['DOMNodeList|false', 'expression'=>'string', 'contextnode='=>'?DOMNode', 'registernodens='=>'bool'], 'DOMXPath::registerNamespace' => ['bool', 'prefix'=>'string', 'namespaceuri'=>'string'], 'DOMXPath::registerPhpFunctions' => ['void', 'restrict='=>'mixed'], 'DomXsltStylesheet::process' => ['DomDocument', 'xml_doc'=>'DOMDocument', 'xslt_params='=>'array', 'is_xpath_param='=>'bool', 'profile_filename='=>'string'], @@ -2021,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'], @@ -2033,7 +2031,7 @@ 'Ds\Deque::clear' => ['void'], 'Ds\Deque::contains' => ['bool', '...values='=>'mixed'], 'Ds\Deque::copy' => ['Ds\Deque'], -'Ds\Deque::count' => ['int'], +'Ds\Deque::count' => ['0|positive-int'], 'Ds\Deque::filter' => ['Ds\Deque', 'callback='=>'callable'], 'Ds\Deque::find' => ['mixed', 'value'=>'mixed'], 'Ds\Deque::first' => ['mixed'], @@ -2068,7 +2066,7 @@ 'Ds\Map::capacity' => ['int'], 'Ds\Map::clear' => ['void'], 'Ds\Map::copy' => ['Ds\Map'], -'Ds\Map::count' => ['int'], +'Ds\Map::count' => ['0|positive-int'], 'Ds\Map::diff' => ['Ds\Map', 'map'=>'Ds\Map'], 'Ds\Map::filter' => ['Ds\Map', 'callback='=>'callable'], 'Ds\Map::first' => ['Ds\Pair'], @@ -2109,7 +2107,7 @@ 'Ds\PriorityQueue::capacity' => ['int'], 'Ds\PriorityQueue::clear' => ['void'], 'Ds\PriorityQueue::copy' => ['Ds\PriorityQueue'], -'Ds\PriorityQueue::count' => ['int'], +'Ds\PriorityQueue::count' => ['0|positive-int'], 'Ds\PriorityQueue::isEmpty' => ['bool'], 'Ds\PriorityQueue::jsonSerialize' => ['array'], 'Ds\PriorityQueue::peek' => ['mixed'], @@ -2121,7 +2119,7 @@ 'Ds\Queue::capacity' => ['int'], 'Ds\Queue::clear' => ['void'], 'Ds\Queue::copy' => ['Ds\Queue'], -'Ds\Queue::count' => ['int'], +'Ds\Queue::count' => ['0|positive-int'], 'Ds\Queue::isEmpty' => ['bool'], 'Ds\Queue::jsonSerialize' => ['array'], 'Ds\Queue::peek' => ['mixed'], @@ -2162,7 +2160,7 @@ 'Ds\Set::clear' => ['void'], 'Ds\Set::contains' => ['bool', '...values='=>'mixed'], 'Ds\Set::copy' => ['Ds\Set'], -'Ds\Set::count' => ['int'], +'Ds\Set::count' => ['0|positive-int'], 'Ds\Set::diff' => ['Ds\Set', 'set'=>'Ds\Set'], 'Ds\Set::filter' => ['Ds\Set', 'callback='=>'callable'], 'Ds\Set::first' => ['mixed'], @@ -2172,6 +2170,7 @@ 'Ds\Set::join' => ['string', 'glue='=>'string'], 'Ds\Set::jsonSerialize' => ['array'], 'Ds\Set::last' => ['mixed'], +'Ds\Set::map' => ['Ds\Set', 'callback='=>'callable'], 'Ds\Set::merge' => ['Ds\Set', 'values'=>'mixed'], 'Ds\Set::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], 'Ds\Set::remove' => ['void', '...values='=>'mixed'], @@ -2189,7 +2188,7 @@ 'Ds\Stack::capacity' => ['int'], 'Ds\Stack::clear' => ['void'], 'Ds\Stack::copy' => ['Ds\Stack'], -'Ds\Stack::count' => ['int'], +'Ds\Stack::count' => ['0|positive-int'], 'Ds\Stack::isEmpty' => ['bool'], 'Ds\Stack::jsonSerialize' => ['array'], 'Ds\Stack::peek' => ['mixed'], @@ -2203,7 +2202,7 @@ 'Ds\Vector::clear' => ['void'], 'Ds\Vector::contains' => ['bool', '...values='=>'mixed'], 'Ds\Vector::copy' => ['Ds\Vector'], -'Ds\Vector::count' => ['int'], +'Ds\Vector::count' => ['0|positive-int'], 'Ds\Vector::filter' => ['Ds\Vector', 'callback='=>'callable'], 'Ds\Vector::find' => ['mixed', 'value'=>'mixed'], 'Ds\Vector::first' => ['mixed'], @@ -2233,7 +2232,6 @@ 'each' => ['array', '&rw_arr'=>'array'], 'easter_date' => ['int', 'year='=>'int'], 'easter_days' => ['int', 'year='=>'int', 'method='=>'int'], -'echo' => ['void', 'arg1'=>'string', '...args='=>'string'], 'eio_busy' => ['resource', 'delay'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_cancel' => ['void', 'req'=>'resource'], 'eio_chmod' => ['resource', 'path'=>'string', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], @@ -2293,7 +2291,6 @@ 'eio_unlink' => ['resource', 'path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_utime' => ['resource', 'path'=>'string', 'atime'=>'float', 'mtime'=>'float', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_write' => ['resource', 'fd'=>'mixed', 'str'=>'string', 'length='=>'int', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'empty' => ['bool', 'var'=>'mixed'], 'EmptyIterator::current' => ['mixed'], 'EmptyIterator::key' => ['mixed'], 'EmptyIterator::next' => ['void'], @@ -2303,19 +2300,19 @@ 'enchant_broker_dict_exists' => ['bool', 'broker'=>'resource', 'tag'=>'string'], 'enchant_broker_free' => ['bool', 'broker'=>'resource'], 'enchant_broker_free_dict' => ['bool', 'dict'=>'resource'], -'enchant_broker_get_dict_path' => ['string', 'broker'=>'resource', 'dict_type'=>'int'], -'enchant_broker_get_error' => ['string', 'broker'=>'resource'], -'enchant_broker_init' => ['resource'], -'enchant_broker_list_dicts' => ['string', 'broker'=>'resource'], -'enchant_broker_request_dict' => ['resource', 'broker'=>'resource', 'tag'=>'string'], -'enchant_broker_request_pwl_dict' => ['resource', 'broker'=>'resource', 'filename'=>'string'], +'enchant_broker_get_dict_path' => ['string|false', 'broker'=>'resource', 'dict_type'=>'int'], +'enchant_broker_get_error' => ['string|false', 'broker'=>'resource'], +'enchant_broker_init' => ['resource|false'], +'enchant_broker_list_dicts' => ['array|false', 'broker'=>'resource'], +'enchant_broker_request_dict' => ['resource|false', 'broker'=>'resource', 'tag'=>'string'], +'enchant_broker_request_pwl_dict' => ['resource|false', 'broker'=>'resource', 'filename'=>'string'], 'enchant_broker_set_dict_path' => ['bool', 'broker'=>'resource', 'dict_type'=>'int', 'value'=>'string'], 'enchant_broker_set_ordering' => ['bool', 'broker'=>'resource', 'tag'=>'string', 'ordering'=>'string'], 'enchant_dict_add_to_personal' => ['void', 'dict'=>'resource', 'word'=>'string'], 'enchant_dict_add_to_session' => ['void', 'dict'=>'resource', 'word'=>'string'], 'enchant_dict_check' => ['bool', 'dict'=>'resource', 'word'=>'string'], 'enchant_dict_describe' => ['array', 'dict'=>'resource'], -'enchant_dict_get_error' => ['string', 'dict'=>'resource'], +'enchant_dict_get_error' => ['string|false', 'dict'=>'resource'], 'enchant_dict_is_in_session' => ['bool', 'dict'=>'resource', 'word'=>'string'], 'enchant_dict_quick_check' => ['bool', 'dict'=>'resource', 'word'=>'string', 'suggestions='=>'array'], 'enchant_dict_store_replacement' => ['void', 'dict'=>'resource', 'mis'=>'string', 'cor'=>'string'], @@ -2333,11 +2330,11 @@ 'Error::getLine' => ['int'], 'Error::getMessage' => ['string'], 'Error::getPrevious' => ['Throwable|Error|null'], -'Error::getTrace' => ['array'], +'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)'], @@ -2348,7 +2345,7 @@ 'ErrorException::getMessage' => ['string'], 'ErrorException::getPrevious' => ['Throwable|ErrorException|null'], 'ErrorException::getSeverity' => ['int'], -'ErrorException::getTrace' => ['array'], +'ErrorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'ErrorException::getTraceAsString' => ['string'], 'escapeshellarg' => ['string', 'arg'=>'string'], 'escapeshellcmd' => ['string', 'command'=>'string'], @@ -2369,7 +2366,6 @@ 'Ev::suspend' => ['void'], 'Ev::time' => ['float'], 'Ev::verify' => ['void'], -'eval' => ['mixed', 'code_str'=>'string'], 'EvCheck::__construct' => ['void', 'callback'=>'callable', 'data='=>'mixed', 'priority='=>'int'], 'EvCheck::createStopped' => ['object', 'callback'=>'string', 'data='=>'string', 'priority='=>'string'], 'EvChild::__construct' => ['void', 'pid'=>'int', 'trace'=>'bool', 'callback'=>'callable', 'data='=>'mixed', 'priority='=>'int'], @@ -2628,21 +2624,20 @@ 'Exception::getLine' => ['int'], 'Exception::getMessage' => ['string'], 'Exception::getPrevious' => ['(?Throwable)|(?Exception)'], -'Exception::getTrace' => ['array'], +'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', 'index'=>'int'], -'exif_thumbnail' => ['string', 'filename'=>'string', '&w_width='=>'int', '&w_height='=>'int', '&w_imagetype='=>'int'], -'exit' => ['', 'status'=>'string|int'], +'exif_tagname' => ['string|false', 'index'=>'int'], +'exif_thumbnail' => ['string|false', 'filename'=>'string', '&w_width='=>'int', '&w_height='=>'int', '&w_imagetype='=>'int'], 'exp' => ['float', 'number'=>'float'], 'expect_expectl' => ['int', 'expect'=>'resource', 'cases'=>'array', 'match='=>'array'], -'expect_popen' => ['resource', 'command'=>'string'], -'explode' => ['array|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], +'expect_popen' => ['resource|false', 'command'=>'string'], +'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'], @@ -2650,7 +2645,7 @@ 'fam_monitor_directory' => ['resource', 'fam'=>'resource', 'dirname'=>'string'], 'fam_monitor_file' => ['resource', 'fam'=>'resource', 'filename'=>'string'], 'fam_next_event' => ['array', 'fam'=>'resource'], -'fam_open' => ['resource', 'appname='=>'string'], +'fam_open' => ['resource|false', 'appname='=>'string'], 'fam_pending' => ['int', 'fam'=>'resource'], 'fam_resume_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_suspend_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], @@ -2879,7 +2874,7 @@ 'fdf_get_version' => ['string', 'fdf_document='=>'resource'], 'fdf_header' => ['void'], 'fdf_next_field_name' => ['string', 'fdf_document'=>'resource', 'fieldname='=>'string'], -'fdf_open' => ['resource', 'filename'=>'string'], +'fdf_open' => ['resource|false', 'filename'=>'string'], 'fdf_open_string' => ['resource', 'fdf_data'=>'string'], 'fdf_remove_item' => ['bool', 'fdf_document'=>'resource', 'fieldname'=>'string', 'item'=>'int'], 'fdf_save' => ['bool', 'fdf_document'=>'resource', 'filename='=>'string'], @@ -2937,13 +2932,13 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['(?array)|(?false)', 'fp'=>'resource', 'length='=>'int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'int'], -'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'int', 'allowable_tags='=>'string'], -'file' => ['array|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], +'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' => ['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='=>'int'], -'file_put_contents' => ['int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'resource'], +'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'maxlen='=>'0|positive-int'], +'file_put_contents' => ['0|positive-int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'?resource'], 'fileatime' => ['int|false', 'filename'=>'string'], 'filectime' => ['int|false', 'filename'=>'string'], 'filegroup' => ['int|false', 'filename'=>'string'], @@ -2958,7 +2953,7 @@ 'filepro_fieldwidth' => ['int', 'field_number'=>'int'], 'filepro_retrieve' => ['string', 'row_number'=>'int', 'field_number'=>'int'], 'filepro_rowcount' => ['int'], -'filesize' => ['int|false', 'filename'=>'string'], +'filesize' => ['0|positive-int|false', 'filename'=>'string'], 'FilesystemIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], 'FilesystemIterator::current' => ['string|SplFileInfo'], 'FilesystemIterator::getFlags' => ['int'], @@ -2970,11 +2965,11 @@ 'filter_has_var' => ['bool', 'type'=>'int', 'variable_name'=>'string'], 'filter_id' => ['int|false', 'filtername'=>'string'], 'filter_input' => ['mixed', 'type'=>'int', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], -'filter_input_array' => ['mixed', 'type'=>'int', 'definition='=>'int|array', 'add_empty='=>'bool'], -'filter_list' => ['array'], +'filter_input_array' => ['array|false|null', 'type'=>'int', 'definition='=>'int|array', 'add_empty='=>'bool'], +'filter_list' => ['non-empty-list'], 'filter_var' => ['mixed', 'variable'=>'mixed', 'filter='=>'int', 'options='=>'mixed'], -'filter_var_array' => ['mixed', 'data'=>'array', 'definition='=>'mixed', 'add_empty='=>'bool'], -'FilterIterator::__construct' => ['void', 'it'=>'iterator'], +'filter_var_array' => ['array|false|null', 'data'=>'array', 'definition='=>'mixed', 'add_empty='=>'bool'], +'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'FilterIterator::accept' => ['bool'], 'FilterIterator::current' => ['mixed'], 'FilterIterator::getInnerIterator' => ['Iterator'], @@ -2992,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', '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' => ['int|false', 'fp'=>'resource'], -'fpm_get_status' => ['array|false'], -'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...args='=>'string|int|float'], -'fputcsv' => ['int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], -'fputs' => ['int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'int'], -'fread' => ['string|false', 'fp'=>'resource', 'length'=>'int'], +'fpassthru' => ['0|positive-int|false', 'fp'=>'resource'], +'fpm_get_status' => ['array{pool: string, process-manager: \'dynamic\'|\'ondemand\'|\'static\', start-time: int<0, max>, start-since: int<0, max>, accepted-conn: int<0, max>, listen-queue: int<0, max>, max-listen-queue: int<0, max>, listen-queue-len: int<0, max>, idle-processes: int<0, max>, active-processes: int<1, max>, total-processes: int<1, max>, max-active-processes: int<1, max>, max-children-reached: 0|1, slow-requests: int<0, max>, procs: array, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}>}|false'], +'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'__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', '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', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], -'fseek' => ['int', 'fp'=>'resource', 'offset'=>'int', 'whence='=>'int'], +'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'], @@ -3024,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', 'ftp_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', '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'], @@ -3050,12 +3045,12 @@ 'ftp_size' => ['int', 'stream'=>'resource', 'filename'=>'string'], 'ftp_ssl_connect' => ['resource|false', 'host'=>'string', 'port='=>'int', 'timeout='=>'int'], 'ftp_systype' => ['string|false', 'stream'=>'resource'], -'ftruncate' => ['bool', 'fp'=>'resource', 'size'=>'int'], -'func_get_arg' => ['mixed', 'arg_num'=>'int'], -'func_get_args' => ['array'], -'func_num_args' => ['int'], +'ftruncate' => ['bool', 'fp'=>'resource', 'size'=>'0|positive-int'], +'func_get_arg' => ['mixed', 'arg_num'=>'0|positive-int'], +'func_get_args' => ['list'], +'func_num_args' => ['0|positive-int'], 'function_exists' => ['bool', 'function_name'=>'string'], -'fwrite' => ['int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'int'], +'fwrite' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], 'gc_collect_cycles' => ['int'], 'gc_disable' => ['void'], 'gc_enable' => ['void'], @@ -3302,60 +3297,60 @@ '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' => ['array', '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'], -'get_included_files' => ['array'], -'get_loaded_extensions' => ['array', 'zend_extensions='=>'bool'], -'get_magic_quotes_gpc' => ['bool'], -'get_magic_quotes_runtime' => ['bool'], -'get_meta_tags' => ['array', 'filename'=>'string', 'use_include_path='=>'bool'], +'get_include_path' => ['__benevolent'], +'get_included_files' => ['list'], +'get_loaded_extensions' => ['list', 'zend_extensions='=>'bool'], +'get_magic_quotes_gpc' => ['false'], +'get_magic_quotes_runtime' => ['false'], +'get_meta_tags' => ['array|false', 'filename'=>'string', 'use_include_path='=>'bool'], 'get_object_vars' => ['array', 'obj'=>'object'], 'get_parent_class' => ['class-string|false', 'object='=>'mixed'], -'get_required_files' => ['string[]'], +'get_required_files' => ['list'], 'get_resource_type' => ['string', 'res'=>'resource'], -'get_resources' => ['resource[]', 'resource_type'=>'string'], +'get_resources' => ['array', 'type='=>'string'], 'getallheaders' => ['array'], -'getcwd' => ['string|false'], -'getdate' => ['array', 'timestamp='=>'int'], +'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'], -'getlastmod' => ['int'], +'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'], -'getmyinode' => ['int'], -'getmypid' => ['int'], -'getmyuid' => ['int'], -'getopt' => ['array|array|array>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], +'getmygid' => ['int|false'], +'getmyinode' => ['int|false'], +'getmypid' => ['int|false'], +'getmyuid' => ['int|false'], +'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], 'getprotobyname' => ['int|false', 'name'=>'string'], -'getprotobynumber' => ['string', 'proto'=>'int'], +'getprotobynumber' => ['string|false', 'proto'=>'int'], 'getrandmax' => ['int'], -'getrusage' => ['array', 'who='=>'int'], +'getrusage' => ['array|false', 'who='=>'int'], 'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'], 'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'], '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' => ['int'], +'GlobIterator::count' => ['0|positive-int'], 'Gmagick::__construct' => ['void', 'filename='=>'string'], 'Gmagick::addimage' => ['Gmagick', 'gmagick'=>'gmagick'], 'Gmagick::addnoiseimage' => ['Gmagick', 'noise'=>'int'], @@ -3538,7 +3533,7 @@ 'GmagickPixel::setcolor' => ['GmagickPixel', 'color'=>'string'], 'GmagickPixel::setcolorvalue' => ['GmagickPixel', 'color'=>'int', 'value'=>'float'], 'gmdate' => ['string', 'format'=>'string', 'timestamp='=>'int'], -'gmmktime' => ['int', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], +'gmmktime' => ['int|false', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], 'GMP::__construct' => ['void'], 'GMP::__toString' => ['string'], 'GMP::serialize' => ['string'], @@ -3550,7 +3545,7 @@ 'gmp_clrbit' => ['void', 'a'=>'GMP|string|int', 'index'=>'int'], 'gmp_cmp' => ['int', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_com' => ['GMP', 'a'=>'GMP|string|int'], -'gmp_div' => ['resource', 'a'=>'GMP|resource|string', 'b'=>'GMP|resource|string', 'round='=>'int'], +'gmp_div' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_q' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_qr' => ['array', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_r' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], @@ -3563,7 +3558,7 @@ 'gmp_import' => ['GMP', 'data'=>'string', 'word_size='=>'int', 'options='=>'int'], 'gmp_init' => ['GMP', 'number'=>'int|string', 'base='=>'int'], 'gmp_intval' => ['int', 'gmpnumber'=>'GMP|string|int'], -'gmp_invert' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], +'gmp_invert' => ['GMP|false', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_jacobi' => ['int', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_kronecker' => ['int', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_lcm' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], @@ -3595,54 +3590,62 @@ 'gmp_sub' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_testbit' => ['bool', 'a'=>'GMP|string|int', 'index'=>'int'], 'gmp_xor' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], -'gmstrftime' => ['string', 'format'=>'string', 'timestamp='=>'int'], +'gmstrftime' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'gnupg::adddecryptkey' => ['bool', 'fingerprint'=>'string', 'passphrase'=>'string'], 'gnupg::addencryptkey' => ['bool', 'fingerprint'=>'string'], 'gnupg::addsignkey' => ['bool', 'fingerprint'=>'string', 'passphrase='=>'string'], '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', '&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', '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'], 'grapheme_stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool'], -'grapheme_strlen' => ['int|false', 'str'=>'string'], +'grapheme_strlen' => ['0|positive-int|false', 'str'=>'string'], 'grapheme_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'grapheme_strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'grapheme_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], @@ -3665,7 +3668,7 @@ 'Grpc\ChannelCredentials::createComposite' => ['Grpc\ChannelCredentials', 'cred1'=>'Grpc\ChannelCredentials', 'cred2'=>'Grpc\CallCredentials'], 'Grpc\ChannelCredentials::createDefault' => ['Grpc\ChannelCredentials'], 'Grpc\ChannelCredentials::createInsecure' => ['null'], -'Grpc\ChannelCredentials::createSsl' => ['Grpc\ChannelCredentials', 'pem_root_certs'=>'string', 'pem_private_key='=>'string', 'pem_cert_chain='=>'string'], +'Grpc\ChannelCredentials::createSsl' => ['Grpc\ChannelCredentials', 'pem_root_certs='=>'string|null', 'pem_private_key='=>'string|null', 'pem_cert_chain='=>'string|null'], 'Grpc\ChannelCredentials::setDefaultRootsPem' => ['', 'pem_roots'=>'string'], 'Grpc\Server::__construct' => ['void', 'args'=>'array'], 'Grpc\Server::addHttp2Port' => ['bool', 'addr'=>'string'], @@ -3727,21 +3730,21 @@ 'gzdecode' => ['string|false', 'data'=>'string', 'length='=>'int'], 'gzdeflate' => ['string|false', 'data'=>'string', 'level='=>'int', 'encoding='=>'int'], 'gzencode' => ['string|false', 'data'=>'string', 'level='=>'int', 'encoding_mode='=>'int'], -'gzeof' => ['int', 'zp'=>'resource'], -'gzfile' => ['array', 'filename'=>'string', 'use_include_path='=>'int'], +'gzeof' => ['bool', 'zp'=>'resource'], +'gzfile' => ['list|false', 'filename'=>'string', 'use_include_path='=>'int'], 'gzgetc' => ['string|false', 'zp'=>'resource'], 'gzgets' => ['string|false', 'zp'=>'resource', 'length='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], 'gzinflate' => ['string|false', 'data'=>'string', 'length='=>'int'], 'gzopen' => ['resource|false', 'filename'=>'string', 'mode'=>'string', 'use_include_path='=>'int'], 'gzpassthru' => ['int|false', 'zp'=>'resource'], -'gzputs' => ['int', 'zp'=>'resource', 'string'=>'string', 'length='=>'int'], -'gzread' => ['string', 'zp'=>'resource', 'length'=>'int'], +'gzputs' => ['int|false', 'zp'=>'resource', 'string'=>'string', 'length='=>'int'], +'gzread' => ['string|false', 'zp'=>'resource', 'length'=>'int'], 'gzrewind' => ['bool', 'zp'=>'resource'], 'gzseek' => ['int', 'zp'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'gztell' => ['int|false', 'zp'=>'resource'], 'gzuncompress' => ['string|false', 'data'=>'string', 'length='=>'int'], -'gzwrite' => ['int', 'zp'=>'resource', 'string'=>'string', 'length='=>'int'], +'gzwrite' => ['int|false', 'zp'=>'resource', 'string'=>'string', 'length='=>'int'], 'HaruAnnotation::setBorderStyle' => ['bool', 'width'=>'float', 'dash_on'=>'int', 'dash_off'=>'int'], 'HaruAnnotation::setHighlightMode' => ['bool', 'mode'=>'int'], 'HaruAnnotation::setIcon' => ['bool', 'icon'=>'int'], @@ -3906,20 +3909,20 @@ 'HaruPage::stroke' => ['bool', 'close_path='=>'bool'], 'HaruPage::textOut' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], 'HaruPage::textRect' => ['bool', 'left'=>'float', 'top'=>'float', 'right'=>'float', 'bottom'=>'float', 'text'=>'string', 'align='=>'int'], -'hash' => ['string', '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' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], -'hash_final' => ['string', 'context'=>'HashContext', 'raw_output='=>'bool'], -'hash_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_hmac_algos' => ['array'], -'hash_hmac_file' => ['string', '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' => ['string', '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_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'scontext='=>'?HashContext'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'handle'=>'resource', 'length='=>'int'], 'hashTableObj::clear' => ['void'], 'hashTableObj::get' => ['string', 'key'=>'string'], @@ -3929,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'], @@ -3954,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'], @@ -4066,7 +4069,7 @@ 'HttpMessage::__construct' => ['void', 'message='=>'string'], 'HttpMessage::__toString' => ['string'], 'HttpMessage::addHeaders' => ['', 'headers'=>'array', 'append='=>'bool'], -'HttpMessage::count' => ['int'], +'HttpMessage::count' => ['0|positive-int'], 'HttpMessage::current' => ['mixed'], 'HttpMessage::detach' => ['HttpMessage'], 'HttpMessage::factory' => ['HttpMessage', 'raw_message='=>'string', 'class_name='=>'string'], @@ -4197,7 +4200,7 @@ 'HttpRequestDataShare::__construct' => ['void'], 'HttpRequestDataShare::__destruct' => [''], 'HttpRequestDataShare::attach' => ['', 'request'=>'HttpRequest'], -'HttpRequestDataShare::count' => ['int'], +'HttpRequestDataShare::count' => ['0|positive-int'], 'HttpRequestDataShare::detach' => ['', 'request'=>'HttpRequest'], 'HttpRequestDataShare::factory' => ['', 'global'=>'', 'class_name'=>''], 'HttpRequestDataShare::reset' => [''], @@ -4205,7 +4208,7 @@ 'HttpRequestPool::__construct' => ['void', 'request='=>'httprequest'], 'HttpRequestPool::__destruct' => [''], 'HttpRequestPool::attach' => ['bool', 'request'=>'httprequest'], -'HttpRequestPool::count' => ['int'], +'HttpRequestPool::count' => ['0|positive-int'], 'HttpRequestPool::current' => ['mixed'], 'HttpRequestPool::detach' => ['bool', 'request'=>'httprequest'], 'HttpRequestPool::enableEvents' => ['', 'enable'=>''], @@ -4312,12 +4315,12 @@ 'hw_api_content' => ['HW_API_Content', 'content'=>'string', 'mimetype'=>'string'], 'hw_api_content::mimetype' => ['string'], 'hw_api_content::read' => ['string', 'buffer'=>'string', 'len'=>'int'], -'hw_api_error::count' => ['int'], +'hw_api_error::count' => ['0|positive-int'], 'hw_api_error::reason' => ['HW_API_Reason'], 'hw_api_object' => ['hw_api_object', 'parameter'=>'array'], 'hw_api_object::assign' => ['bool', 'parameter'=>'array'], 'hw_api_object::attreditable' => ['bool', 'parameter'=>'array'], -'hw_api_object::count' => ['int', 'parameter'=>'array'], +'hw_api_object::count' => ['0|positive-int', 'parameter'=>'array'], 'hw_api_object::insert' => ['bool', 'attribute'=>'hw_api_attribute'], 'hw_api_object::remove' => ['bool', 'name'=>'string'], 'hw_api_object::title' => ['string', 'parameter'=>'array'], @@ -4404,7 +4407,7 @@ 'ibase_blob_import' => ['string', 'link_identifier'=>'', 'file_handle'=>''], 'ibase_blob_info' => ['array', 'link_identifier'=>'', 'blob_id'=>'string'], 'ibase_blob_info\'1' => ['array', 'blob_id'=>'string'], -'ibase_blob_open' => ['resource', 'link_identifier'=>'', 'blob_id'=>'string'], +'ibase_blob_open' => ['resource|false', 'link_identifier'=>'', 'blob_id'=>'string'], 'ibase_blob_open\'1' => ['resource', 'blob_id'=>'string'], 'ibase_close' => ['bool', 'link_identifier='=>'resource'], 'ibase_commit' => ['bool', 'link_identifier='=>'resource'], @@ -4452,7 +4455,7 @@ 'iconv_mime_decode_headers' => ['array|false', 'headers'=>'string', 'mode='=>'int', 'charset='=>'string'], 'iconv_mime_encode' => ['string|false', 'field_name'=>'string', 'field_value'=>'string', 'preference='=>'array'], 'iconv_set_encoding' => ['bool', 'type'=>'string', 'charset'=>'string'], -'iconv_strlen' => ['int', 'str'=>'string', 'charset='=>'string'], +'iconv_strlen' => ['0|positive-int|false', 'str'=>'string', 'charset='=>'string'], 'iconv_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'charset='=>'string'], 'iconv_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'charset='=>'string'], 'iconv_substr' => ['string|false', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'charset='=>'string'], @@ -4465,7 +4468,7 @@ 'id3_get_version' => ['int', 'filename'=>'string'], 'id3_remove_tag' => ['bool', 'filename'=>'string', 'version='=>'int'], 'id3_set_tag' => ['bool', 'filename'=>'string', 'tag'=>'array', 'version='=>'int'], -'idate' => ['int', 'format'=>'string', 'timestamp='=>'int'], +'idate' => ['int|false', 'format'=>'string', 'timestamp='=>'int'], 'idn_strerror' => ['string', 'errorcode'=>'int'], 'idn_to_ascii' => ['string|false', 'domain'=>'string', 'options='=>'int', 'variant='=>'int', '&w_idna_info='=>'array'], 'idn_to_utf8' => ['string|false', 'domain'=>'string', 'options='=>'int', 'variant='=>'int', '&w_idna_info='=>'array'], @@ -4507,9 +4510,9 @@ 'ifxus_seek_slob' => ['int', 'bid'=>'int', 'mode'=>'int', 'offset'=>'int'], 'ifxus_tell_slob' => ['int', 'bid'=>'int'], 'ifxus_write_slob' => ['int', 'bid'=>'int', 'content'=>'string'], -'igbinary_serialize' => ['string', 'value'=>''], -'igbinary_unserialize' => ['', 'str'=>'string'], -'ignore_user_abort' => ['int', 'value='=>'bool'], +'igbinary_serialize' => ['string|null', 'value'=>'mixed'], +'igbinary_unserialize' => ['mixed', 'str'=>'string'], +'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'], @@ -4527,33 +4530,33 @@ 'iis_stop_server' => ['int', 'server_instance'=>'int'], 'iis_stop_service' => ['int', 'service_id'=>'string'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], -'image_type_to_extension' => ['string', 'imagetype'=>'int', 'include_dot='=>'bool'], +'image_type_to_extension' => ['string|false', 'imagetype'=>'int', 'include_dot='=>'bool'], 'image_type_to_mime_type' => ['string', 'imagetype'=>'int'], -'imageaffine' => ['resource', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], +'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imageaffineconcat' => ['array', 'm1'=>'array', 'm2'=>'array'], 'imageaffinematrixconcat' => ['array{0:float,1:float,2:float,3:float,4:float,5:float}|false', 'm1'=>'array', 'm2'=>'array'], 'imageaffinematrixget' => ['array{0:float,1:float,2:float,3:float,4:float,5:float}|false', 'type'=>'int', 'options'=>'array|float'], 'imagealphablending' => ['bool', 'im'=>'resource', 'on'=>'bool'], 'imageantialias' => ['bool', 'im'=>'resource', 'on'=>'bool'], 'imagearc' => ['bool', 'im'=>'resource', 'cx'=>'int', 'cy'=>'int', 'w'=>'int', 'h'=>'int', 's'=>'int', 'e'=>'int', 'col'=>'int'], -'imagebmp' => ['bool', 'image'=>'resource', 'to='=>'mixed', 'compressed='=>'bool'], +'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', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorallocatealpha' => ['int', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorat' => ['int', 'im'=>'resource', 'x'=>'int', 'y'=>'int'], -'imagecolorclosest' => ['int', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorclosestalpha' => ['int', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorclosesthwb' => ['int', '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', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorexactalpha' => ['int', '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', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorresolvealpha' => ['int', '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', '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'], @@ -4561,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'], @@ -4574,9 +4577,9 @@ 'imagecreatefromwebp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxbm' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxpm' => ['resource|false', 'filename'=>'string'], -'imagecreatetruecolor' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], -'imagecrop' => ['resource', 'im'=>'resource', 'rect'=>'array'], -'imagecropauto' => ['resource', 'im'=>'resource', 'mode='=>'int', 'threshold='=>'float', 'color='=>'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'], 'imagedestroy' => ['bool', 'im'=>'resource'], 'imageellipse' => ['bool', 'im'=>'resource', 'cx'=>'int', 'cy'=>'int', 'w'=>'int', 'h'=>'int', 'color'=>'int'], @@ -4590,23 +4593,23 @@ 'imageflip' => ['bool', 'im'=>'resource', 'mode'=>'int'], 'imagefontheight' => ['int', 'font'=>'int'], 'imagefontwidth' => ['int', 'font'=>'int'], -'imageftbbox' => ['array', 'size'=>'float', 'angle'=>'float', 'font_file'=>'string', 'text'=>'string', 'extrainfo='=>'array'], -'imagefttext' => ['array', 'im'=>'resource', 'size'=>'float', 'angle'=>'float', 'x'=>'int', 'y'=>'int', 'col'=>'int', 'font_file'=>'string', 'text'=>'string', 'extrainfo='=>'array'], +'imageftbbox' => ['array|false', 'size'=>'float', 'angle'=>'float', 'font_file'=>'string', 'text'=>'string', 'extrainfo='=>'array'], +'imagefttext' => ['array|false', 'im'=>'resource', 'size'=>'float', 'angle'=>'float', 'x'=>'int', 'y'=>'int', 'col'=>'int', 'font_file'=>'string', 'text'=>'string', 'extrainfo='=>'array'], 'imagegammacorrect' => ['bool', 'im'=>'resource', 'inputgamma'=>'float', 'outputgamma'=>'float'], 'imagegd' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null'], 'imagegd2' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'chunk_size='=>'int', 'type='=>'int'], 'imagegetclip' => ['array', 'im'=>'resource'], 'imagegif' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null'], -'imagegrabscreen' => ['resource'], -'imagegrabwindow' => ['resource', 'window_handle'=>'int', 'client_area='=>'int'], +'imagegrabscreen' => ['resource|false'], +'imagegrabwindow' => ['resource|false', 'window_handle'=>'int', 'client_area='=>'int'], 'imageinterlace' => ['int', 'im'=>'resource', 'interlace='=>'int'], 'imageistruecolor' => ['bool', 'im'=>'resource'], 'imagejpeg' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'quality='=>'int'], 'imagelayereffect' => ['bool', 'im'=>'resource', 'effect'=>'int'], 'imageline' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int', 'col'=>'int'], -'imageloadfont' => ['int', 'filename'=>'string'], +'imageloadfont' => ['int|false', 'filename'=>'string'], 'imageObj::pasteImage' => ['void', 'srcImg'=>'imageObj', 'transparentColorHex'=>'int', 'dstX'=>'int', 'dstY'=>'int', 'angle'=>'int'], -'imageObj::saveImage' => ['int', 'filename'=>'string', 'oMap'=>'MapObj'], +'imageObj::saveImage' => ['int', 'filename'=>'string', 'oMap'=>'mapObj'], 'imageObj::saveWebImage' => ['string'], 'imageopenpolygon' => ['bool', 'image'=>'resource', 'points'=>'array', 'num_points'=>'int', 'color'=>'int'], 'imagepalettecopy' => ['void', 'dst'=>'resource', 'src'=>'resource'], @@ -4622,9 +4625,9 @@ 'imagepstext' => ['array', 'image'=>'resource', 'text'=>'string', 'font_index'=>'resource', 'size'=>'int', 'foreground'=>'int', 'background'=>'int', 'x'=>'int', 'y'=>'int', 'space='=>'int', 'tightness='=>'int', 'angle='=>'float', 'antialias_steps='=>'int'], 'imagerectangle' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int', 'col'=>'int'], 'imageresolution' => ['mixed', 'image'=>'resource', 'res_x='=>'int', 'res_y='=>'int'], -'imagerotate' => ['resource', 'src_im'=>'resource', 'angle'=>'float', 'bgdcolor'=>'int', 'ignoretransparent='=>'int'], +'imagerotate' => ['resource|false', 'src_im'=>'resource', 'angle'=>'float', 'bgdcolor'=>'int', 'ignoretransparent='=>'int'], 'imagesavealpha' => ['bool', 'im'=>'resource', 'on'=>'bool'], -'imagescale' => ['resource', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], +'imagescale' => ['resource|false', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], 'imagesetbrush' => ['bool', 'image'=>'resource', 'brush'=>'resource'], 'imagesetclip' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int'], 'imagesetinterpolation' => ['bool', 'im'=>'resource', 'method'=>'int'], @@ -4634,60 +4637,60 @@ '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', 'size'=>'float', 'angle'=>'float', 'font_file'=>'string', 'text'=>'string'], -'imagettftext' => ['array', 'im'=>'resource', 'size'=>'float', 'angle'=>'float', 'x'=>'int', 'y'=>'int', 'col'=>'int', 'font_file'=>'string', 'text'=>'string'], +'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'], 'imagetypes' => ['int'], 'imagewbmp' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'foreground='=>'int'], 'imagewebp' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'quality='=>'int'], -'imagexbm' => ['bool', 'im'=>'resource', 'filename'=>'string|resource|null', 'foreground='=>'int'], +'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' => ['void', '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' => ['void', 'factor='=>'float'], -'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], +'Imagick::blueShiftImage' => ['bool', 'factor='=>'float'], +'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::borderImage' => ['bool', 'bordercolor'=>'mixed', 'width'=>'int', 'height'=>'int'], -'Imagick::brightnessContrastImage' => ['void', 'brightness'=>'string', 'contrast'=>'string', 'CHANNEL='=>'string'], +'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' => ['void', 'CHANNEL='=>'string'], +'Imagick::clampImage' => ['bool', 'channel='=>'int'], 'Imagick::clear' => ['bool'], 'Imagick::clipImage' => ['bool'], 'Imagick::clipImagePath' => ['void', 'pathname'=>'string', 'inside'=>'string'], 'Imagick::clipPathImage' => ['bool', 'pathname'=>'string', 'inside'=>'bool'], 'Imagick::clone' => ['Imagick'], -'Imagick::clutImage' => ['bool', 'lookup_table'=>'imagick', 'channel='=>'float'], +'Imagick::clutImage' => ['bool', 'lookup_table'=>'imagick', 'int='=>'float'], 'Imagick::coalesceImages' => ['Imagick'], 'Imagick::colorFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int'], 'Imagick::colorizeImage' => ['bool', 'colorize'=>'mixed', 'opacity'=>'mixed'], -'Imagick::colorMatrixImage' => ['void', 'color_matrix'=>'string'], -'Imagick::combineImages' => ['Imagick', 'channeltype'=>'int'], +'Imagick::colorMatrixImage' => ['bool', 'color_matrix'=>'array'], +'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::count' => ['void', 'mode='=>'string'], +'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'], 'Imagick::current' => ['Imagick'], @@ -4701,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' => ['void', 'ImagickKernel'=>'ImagickKernel', 'CHANNEL='=>'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' => ['void', 'magnitude'=>'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::inverseFourierTransformImage' => ['void', 'complement'=>'string', 'magnitude'=>'string'], +'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' => ['bool', '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' => ['void', 'morphologyMethod'=>'int', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'CHANNEL='=>'string'], +'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'], @@ -4870,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'], @@ -4896,70 +4899,70 @@ 'Imagick::rewind' => ['void'], 'Imagick::rollImage' => ['bool', 'x'=>'int', 'y'=>'int'], 'Imagick::rotateImage' => ['bool', 'background'=>'mixed', 'degrees'=>'float'], -'Imagick::rotationalBlurImage' => ['void', 'angle'=>'string', 'CHANNEL='=>'string'], +'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' => ['void', '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' => ['void', 'key'=>'string', 'value'=>'string'], +'Imagick::setImageAttribute' => ['bool', 'key'=>'string', 'value'=>'string'], 'Imagick::setImageBackgroundColor' => ['bool', 'background'=>'mixed'], 'Imagick::setImageBias' => ['bool', 'bias'=>'float'], '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'], @@ -4969,53 +4972,53 @@ 'Imagick::setOption' => ['bool', 'key'=>'string', 'value'=>'string'], 'Imagick::setPage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::setPointSize' => ['bool', 'point_size'=>'float'], -'Imagick::setProgressMonitor' => ['void', 'callback'=>'callable'], -'Imagick::setRegistry' => ['void', 'key'=>'string', 'value'=>'string'], +'Imagick::setProgressMonitor' => ['bool', 'callback'=>'callable'], +'Imagick::setRegistry' => ['bool', 'key'=>'string', 'value'=>'string'], 'Imagick::setResolution' => ['bool', 'x_resolution'=>'float', 'y_resolution'=>'float'], 'Imagick::setResourceLimit' => ['bool', 'type'=>'int', 'limit'=>'int'], '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'=>'string', 'offset'=>'string'], -'Imagick::solarizeImage' => ['bool', 'threshold'=>'int'], -'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'int', 'arguments'=>'array', 'channel='=>'int'], +'Imagick::smushImages' => ['Imagick', 'stack'=>'bool', 'offset'=>'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' => ['void', 'type'=>'int', 'width'=>'int', 'height'=>'int', 'CHANNEL='=>'string'], +'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' => ['bool', 'texture_wand'=>'imagick'], -'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'int'], +'Imagick::textureImage' => ['Imagick', 'texture_wand'=>'imagick'], +'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'], @@ -5024,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'], @@ -5036,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'], @@ -5066,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'], @@ -5107,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'], @@ -5130,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'], @@ -5152,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'], @@ -5213,7 +5215,7 @@ 'imap_close' => ['bool', 'stream_id'=>'resource', 'options='=>'int'], 'imap_create' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], -'imap_delete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'int', 'options='=>'int'], +'imap_delete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'string', 'options='=>'int'], 'imap_deletemailbox' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], 'imap_expunge' => ['bool', 'stream_id'=>'resource'], @@ -5248,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'], @@ -5270,7 +5272,7 @@ 'imap_thread' => ['array|false', 'stream_id'=>'resource', 'options='=>'int'], 'imap_timeout' => ['mixed', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'stream_id'=>'resource', 'msg_no'=>'int'], -'imap_undelete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'int', 'flags='=>'int'], +'imap_undelete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'buf'=>'string'], 'imap_utf7_encode' => ['string', 'buf'=>'string'], @@ -5284,7 +5286,7 @@ 'inclued_get_data' => ['array'], 'inet_ntop' => ['string|false', 'in_addr'=>'string'], 'inet_pton' => ['string|false', 'ip_address'=>'string'], -'InfiniteIterator::__construct' => ['void', 'it'=>'iterator'], +'InfiniteIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'InfiniteIterator::next' => ['void'], 'inflate_add' => ['string|false', 'context'=>'resource', 'encoded_data'=>'string', 'flush_mode='=>'int'], 'inflate_get_read_len' => ['int|false', 'resource'=>'resource'], @@ -5329,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'], @@ -5353,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'], @@ -5376,7 +5378,7 @@ 'intlcal_get_day_of_week_type' => ['int', 'cal'=>'IntlCalendar', 'dayOfWeek'=>'int'], 'intlcal_get_first_day_of_week' => ['int', 'cal'=>'IntlCalendar'], 'intlcal_get_greatest_minimum' => ['int', 'cal'=>'IntlCalendar', 'field'=>'int'], -'intlcal_get_keyword_values_for_locale' => ['Iterator', 'key'=>'string', 'locale'=>'string', 'commonlyUsed'=>'bool'], +'intlcal_get_keyword_values_for_locale' => ['Iterator|false', 'key'=>'string', 'locale'=>'string', 'commonlyUsed'=>'bool'], 'intlcal_get_least_maximum' => ['int', 'cal'=>'IntlCalendar', 'field'=>'int'], 'intlcal_get_locale' => ['string', 'cal'=>'IntlCalendar', 'localeType'=>'int'], 'intlcal_get_maximum' => ['int', 'cal'=>'IntlCalendar', 'field'=>'int'], @@ -5386,7 +5388,7 @@ 'intlcal_get_repeated_wall_time_option' => ['int', 'cal'=>'IntlCalendar'], 'intlcal_get_skipped_wall_time_option' => ['int', 'cal'=>'IntlCalendar'], 'intlcal_get_time' => ['float', 'cal'=>'IntlCalendar'], -'intlcal_get_time_zone' => ['IntlTimeZone', 'cal'=>'IntlCalendar'], +'intlcal_get_time_zone' => ['IntlTimeZone|false', 'cal'=>'IntlCalendar'], 'intlcal_get_type' => ['string', 'cal'=>'IntlCalendar'], 'intlcal_get_weekend_transition' => ['int', 'cal'=>'IntlCalendar', 'dayOfWeek'=>'string'], 'intlcal_in_daylight_time' => ['bool', 'cal'=>'IntlCalendar'], @@ -5403,7 +5405,7 @@ 'intlcal_set_skipped_wall_time_option' => ['bool', 'cal'=>'IntlCalendar', 'wallTimeOption'=>'int'], 'intlcal_set_time' => ['bool', 'cal'=>'IntlCalendar', 'date'=>'float'], 'intlcal_set_time_zone' => ['bool', 'cal'=>'IntlCalendar', 'timeZone'=>'mixed'], -'intlcal_to_date_time' => ['DateTime', 'cal'=>'IntlCalendar'], +'intlcal_to_date_time' => ['DateTime|false', 'cal'=>'IntlCalendar'], 'IntlCalendar::__construct' => ['void'], 'IntlCalendar::add' => ['bool', 'field'=>'int', 'amount'=>'int'], 'IntlCalendar::after' => ['bool', 'other'=>'IntlCalendar'], @@ -5513,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::format' => ['string', 'args'=>''], -'IntlDateFormatter::formatObject' => ['string', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], -'IntlDateFormatter::getCalendar' => ['int'], -'IntlDateFormatter::getCalendarObject' => ['IntlCalendar'], -'IntlDateFormatter::getDateType' => ['int'], +'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|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', '&rw_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'], @@ -5542,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'], @@ -5592,26 +5595,26 @@ 'IntlTimeZone::hasSameRules' => ['bool', 'otherTimeZone'=>'IntlTimeZone'], 'IntlTimeZone::toDateTimeZone' => ['DateTimeZone'], 'IntlTimeZone::useDaylightTime' => ['bool'], -'intltz_count_equivalent_ids' => ['int', 'zoneId'=>'string'], +'intltz_count_equivalent_ids' => ['int|false', 'zoneId'=>'string'], 'intltz_create_enumeration' => ['IntlIterator', 'countryOrRawOffset'=>'mixed'], 'intltz_create_time_zone' => ['IntlTimeZone', 'zoneId'=>'string'], 'intltz_from_date_time_zone' => ['IntlTimeZone', 'zoneId'=>'DateTimeZone'], -'intltz_get_canonical_id' => ['string', 'zoneId'=>'string', '&isSystemID'=>'bool'], -'intltz_get_display_name' => ['string', 'obj'=>'IntlTimeZone', 'isDaylight'=>'bool', 'style'=>'int', 'locale'=>'string'], +'intltz_get_canonical_id' => ['string|false', 'zoneId'=>'string', '&isSystemID'=>'bool'], +'intltz_get_display_name' => ['string|false', 'obj'=>'IntlTimeZone', 'isDaylight'=>'bool', 'style'=>'int', 'locale'=>'string'], 'intltz_get_dst_savings' => ['int', 'obj'=>'IntlTimeZone'], -'intltz_get_equivalent_id' => ['string', 'zoneId'=>'string', 'index'=>'int'], -'intltz_get_error_code' => ['int', 'obj'=>'IntlTimeZone'], -'intltz_get_error_message' => ['string', 'obj'=>'IntlTimeZone'], -'intltz_get_id' => ['string', 'obj'=>'IntlTimeZone'], +'intltz_get_equivalent_id' => ['string|false', 'zoneId'=>'string', 'index'=>'int'], +'intltz_get_error_code' => ['int|false', 'obj'=>'IntlTimeZone'], +'intltz_get_error_message' => ['string|false', 'obj'=>'IntlTimeZone'], +'intltz_get_id' => ['string|false', 'obj'=>'IntlTimeZone'], 'intltz_get_offset' => ['int', 'obj'=>'IntlTimeZone', 'date'=>'float', 'local'=>'bool', '&rawOffset'=>'int', '&dstOffset'=>'int'], 'intltz_get_raw_offset' => ['int', 'obj'=>'IntlTimeZone'], -'intltz_get_tz_data_version' => ['string', 'obj'=>'IntlTimeZone'], +'intltz_get_tz_data_version' => ['string|false', 'obj'=>'IntlTimeZone'], 'intltz_getGMT' => ['IntlTimeZone'], 'intltz_has_same_rules' => ['bool', 'obj'=>'IntlTimeZone', 'otherTimeZone'=>'IntlTimeZone'], -'intltz_to_date_time_zone' => ['DateTimeZone', 'obj'=>''], +'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'], @@ -5620,11 +5623,11 @@ 'InvalidArgumentException::getLine' => ['int'], 'InvalidArgumentException::getMessage' => ['string'], 'InvalidArgumentException::getPrevious' => ['Throwable|InvalidArgumentException|null'], -'InvalidArgumentException::getTrace' => ['array'], +'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'], @@ -5657,17 +5660,16 @@ 'is_uploaded_file' => ['bool', 'path'=>'string'], 'is_writable' => ['bool', 'filename'=>'string'], 'is_writeable' => ['bool', 'filename'=>'string'], -'isset' => ['bool', 'var'=>'mixed', '...rest='=>'mixed'], 'Iterator::current' => ['mixed'], 'Iterator::key' => ['mixed'], 'Iterator::next' => ['void'], 'Iterator::rewind' => ['void'], 'Iterator::valid' => ['bool'], -'iterator_apply' => ['int', 'it'=>'Traversable', 'function'=>'callable', 'params='=>'array'], -'iterator_count' => ['int', 'it'=>'Traversable'], -'iterator_to_array' => ['array', 'it'=>'Traversable', 'use_keys='=>'bool'], +'iterator_apply' => ['0|positive-int', 'iterator'=>'Traversable', 'function'=>'callable', 'params='=>'array'], +'iterator_count' => ['0|positive-int', 'iterator'=>'Traversable'], +'iterator_to_array' => ['array', 'iterator'=>'Traversable', 'use_keys='=>'bool'], 'IteratorAggregate::getIterator' => ['Traversable'], -'IteratorIterator::__construct' => ['void', 'it'=>'Traversable'], +'IteratorIterator::__construct' => ['void', 'iterator'=>'Traversable'], 'IteratorIterator::current' => ['mixed'], 'IteratorIterator::getInnerIterator' => ['Traversable'], 'IteratorIterator::key' => ['mixed'], @@ -5694,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='=>'int', 'options='=>'int'], -'json_encode' => ['string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'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'=>''], @@ -5708,7 +5710,7 @@ 'Judy::__construct' => ['void', 'judy_type'=>'int'], 'Judy::__destruct' => [''], 'Judy::byCount' => ['int', 'nth_index'=>'int'], -'Judy::count' => ['int', 'index_start='=>'int', 'index_end='=>'int'], +'Judy::count' => ['0|positive-int', 'index_start='=>'int', 'index_end='=>'int'], 'Judy::first' => ['mixed', 'index='=>'mixed'], 'Judy::firstEmpty' => ['int', 'index='=>'mixed'], 'Judy::free' => ['int'], @@ -5738,7 +5740,7 @@ 'kadm5_get_principals' => ['array', 'handle'=>'resource'], 'kadm5_init_with_password' => ['resource', 'admin_server'=>'string', 'realm'=>'string', 'principal'=>'string', 'password'=>'string'], 'kadm5_modify_principal' => ['bool', 'handle'=>'resource', 'principal'=>'string', 'options'=>'array'], -'key' => ['int|string|null', 'array_arg'=>'array'], +'key' => ['int|string|null', 'array_arg'=>'array|object'], 'key_exists' => ['bool', 'key'=>'string|int', 'search'=>'array'], 'krsort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], 'ksort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], @@ -5824,7 +5826,7 @@ 'layerObj::isVisible' => ['bool'], 'layerObj::moveclassdown' => ['int', 'index'=>'int'], 'layerObj::moveclassup' => ['int', 'index'=>'int'], -'layerObj::ms_newLayerObj' => ['layerObj', 'map'=>'MapObj', 'layer'=>'layerObj'], +'layerObj::ms_newLayerObj' => ['layerObj', 'map'=>'mapObj', 'layer'=>'layerObj'], 'layerObj::nextShape' => ['shapeObj'], 'layerObj::open' => ['int'], 'layerObj::queryByAttributes' => ['int', 'qitem'=>'string', 'qstring'=>'string', 'mode'=>'int'], @@ -5845,64 +5847,64 @@ 'lcg_value' => ['float'], 'lchgrp' => ['bool', 'filename'=>'string', 'group'=>'string|int'], 'lchown' => ['bool', 'filename'=>'string', 'user'=>'string|int'], -'ldap_8859_to_t61' => ['string', 'value'=>'string'], +'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'], +'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'], '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_count_entries' => ['int', 'link_identifier'=>'resource', 'result'=>'resource'], -'ldap_delete' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string'], +'ldap_delete' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'servercontrols='=>'array'], 'ldap_delete_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'servercontrols='=>'array'], -'ldap_dn2ufn' => ['string', 'dn'=>'string'], +'ldap_dn2ufn' => ['string|false', 'dn'=>'string'], 'ldap_err2str' => ['string', 'errno'=>'int'], 'ldap_errno' => ['int', 'link_identifier'=>'resource'], 'ldap_error' => ['string', 'link_identifier'=>'resource'], 'ldap_escape' => ['string', 'value'=>'string', 'ignore='=>'string', 'flags='=>'int'], -'ldap_exop' => ['mixed', 'link'=>'resource', 'reqoid'=>'string', 'reqdata='=>'string', 'serverctrls='=>'array|null', '&w_retdata='=>'string', '&w_retoid='=>'string'], -'ldap_exop_passwd' => ['mixed', 'link'=>'resource', 'user='=>'string', 'oldpw='=>'string', 'newpw='=>'string', 'serverctrls='=>'array'], +'ldap_exop' => ['resource|bool', 'link'=>'resource', 'reqoid'=>'string', 'reqdata='=>'string', 'serverctrls='=>'array|null', '&w_retdata='=>'string', '&w_retoid='=>'string'], +'ldap_exop_passwd' => ['string|bool', 'link'=>'resource', 'user='=>'string', 'oldpw='=>'string', 'newpw='=>'string', 'serverctrls='=>'array'], 'ldap_exop_refresh' => ['int|false', 'link'=>'resource', 'dn'=>'string', 'ttl'=>'int'], -'ldap_exop_whoami' => ['string', 'link'=>'resource'], -'ldap_explode_dn' => ['array', 'dn'=>'string', 'with_attrib'=>'int'], -'ldap_first_attribute' => ['string', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], +'ldap_exop_whoami' => ['string|bool', 'link'=>'resource'], +'ldap_explode_dn' => ['array|false', 'dn'=>'string', 'with_attrib'=>'int'], +'ldap_first_attribute' => ['string|false', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], 'ldap_first_entry' => ['resource|false', 'link_identifier'=>'resource', 'result_identifier'=>'resource'], 'ldap_first_reference' => ['resource|false', 'link_identifier'=>'resource', 'result_identifier'=>'resource'], 'ldap_free_result' => ['bool', 'result_identifier'=>'resource'], 'ldap_get_attributes' => ['array', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], -'ldap_get_dn' => ['string', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], +'ldap_get_dn' => ['string|false', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], 'ldap_get_entries' => ['array|false', 'link_identifier'=>'resource', 'result_identifier'=>'resource'], 'ldap_get_option' => ['bool', 'link_identifier'=>'resource', 'option'=>'int', '&w_retval'=>'mixed'], -'ldap_get_values' => ['array', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource', 'attribute'=>'string'], -'ldap_get_values_len' => ['array', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource', 'attribute'=>'string'], -'ldap_list' => ['resource|false', 'link'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], -'ldap_mod_add' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array'], +'ldap_get_values' => ['array|false', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource', 'attribute'=>'string'], +'ldap_get_values_len' => ['array|false', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource', 'attribute'=>'string'], +'ldap_list' => ['resource|false', 'link'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'servercontrols='=>'array'], +'ldap_mod_add' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_mod_add_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], -'ldap_mod_del' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array'], +'ldap_mod_del' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_mod_del_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], -'ldap_mod_replace' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array'], +'ldap_mod_replace' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_mod_replace_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_modify' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array'], -'ldap_modify_batch' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'modifs'=>'array'], -'ldap_next_attribute' => ['string', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], +'ldap_modify_batch' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'modifs'=>'array', 'servercontrols='=>'array'], +'ldap_next_attribute' => ['string|false', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], 'ldap_next_entry' => ['resource|false', 'link_identifier'=>'resource', 'result_entry_identifier'=>'resource'], 'ldap_next_reference' => ['resource|false', 'link_identifier'=>'resource', 'reference_entry_identifier'=>'resource'], 'ldap_parse_exop' => ['bool', 'link'=>'resource', 'result'=>'resource', '&w_retdata='=>'string', '&w_retoid='=>'string'], 'ldap_parse_reference' => ['bool', 'link_identifier'=>'resource', 'reference_entry_identifier'=>'resource', 'referrals'=>'array'], 'ldap_parse_result' => ['bool', 'link_identifier'=>'resource', 'result'=>'resource', '&w_errcode'=>'int', '&w_matcheddn='=>'string', '&w_errmsg='=>'string', '&w_referrals='=>'array', '&w_serverctrls='=>'array'], -'ldap_read' => ['resource|false', 'link'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], -'ldap_rename' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'newrdn'=>'string', 'newparent'=>'string', 'deleteoldrdn'=>'bool'], +'ldap_read' => ['resource|false', 'link'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'servercontrols='=>'array'], +'ldap_rename' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'newrdn'=>'string', 'newparent'=>'string', 'deleteoldrdn'=>'bool', 'servercontrols='=>'array'], 'ldap_rename_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'newrdn'=>'string', 'newparent'=>'string', 'deleteoldrdn'=>'bool', 'servercontrols='=>'array'], 'ldap_sasl_bind' => ['bool', 'link_identifier'=>'resource', 'binddn='=>'string', 'password='=>'string', 'sasl_mech='=>'string', 'sasl_realm='=>'string', 'sasl_authc_id='=>'string', 'sasl_authz_id='=>'string', 'props='=>'string'], -'ldap_search' => ['resource|false', 'link_identifier'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], +'ldap_search' => ['resource|false', 'link_identifier'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'servercontrols='=>'array'], 'ldap_set_option' => ['bool', 'link_identifier'=>'resource|null', 'option'=>'int', 'newval'=>'mixed'], -'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'string'], +'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'callable'], 'ldap_sort' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', 'sortfilter'=>'string'], 'ldap_start_tls' => ['bool', 'link_identifier'=>'resource'], -'ldap_t61_to_8859' => ['string', 'value'=>'string'], +'ldap_t61_to_8859' => ['string|false', 'value'=>'string'], 'ldap_unbind' => ['bool', 'link_identifier'=>'resource'], 'leak' => ['', 'num_bytes'=>'int'], 'leak_variable' => ['', 'variable'=>'', 'leak_data'=>'bool'], @@ -5918,7 +5920,7 @@ 'LengthException::getLine' => ['int'], 'LengthException::getMessage' => ['string'], 'LengthException::getPrevious' => ['Throwable|LengthException|null'], -'LengthException::getTrace' => ['array'], +'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'], @@ -5946,44 +5948,44 @@ 'lineObj::point' => ['pointObj', 'i'=>'int'], 'lineObj::project' => ['int', 'in'=>'projectionObj', 'out'=>'projectionObj'], 'link' => ['bool', 'target'=>'string', 'link'=>'string'], -'linkinfo' => ['int', 'filename'=>'string'], +'linkinfo' => ['int|false', 'filename'=>'string'], 'litespeed_request_headers' => ['array'], -'litespeed_response_headers' => ['array'], -'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'], +'litespeed_response_headers' => ['array|false'], +'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', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_name' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_region' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_script' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_variant' => ['string', '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'], @@ -5998,9 +6000,9 @@ 'LogicException::getLine' => ['int'], 'LogicException::getMessage' => ['string'], 'LogicException::getPrevious' => ['Throwable|LogicException|null'], -'LogicException::getTrace' => ['array'], +'LogicException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'LogicException::getTraceAsString' => ['string'], -'long2ip' => ['string', 'proper_address'=>'int'], +'long2ip' => ['string|false', 'proper_address'=>'int'], 'lstat' => ['array|false', 'filename'=>'string'], 'ltrim' => ['string', 'str'=>'string', 'character_mask='=>'string'], 'Lua::__call' => ['mixed', 'lua_func'=>'callable', 'args='=>'array', 'use_self='=>'int'], @@ -6062,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'], @@ -6105,7 +6107,7 @@ 'mapObj::loadOWSParameters' => ['int', 'request'=>'OwsrequestObj', 'version'=>'string'], 'mapObj::moveLayerDown' => ['int', 'layerindex'=>'int'], 'mapObj::moveLayerUp' => ['int', 'layerindex'=>'int'], -'mapObj::ms_newMapObjFromString' => ['MapObj', 'map_file_string'=>'string', 'new_map_path'=>'string'], +'mapObj::ms_newMapObjFromString' => ['mapObj', 'map_file_string'=>'string', 'new_map_path'=>'string'], 'mapObj::offsetExtent' => ['int', 'x'=>'float', 'y'=>'float'], 'mapObj::owsDispatch' => ['int', 'request'=>'OwsrequestObj'], 'mapObj::prepareImage' => ['imageObj'], @@ -6140,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'=>''], @@ -6307,23 +6309,23 @@ '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', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], +'mb_convert_encoding' => ['string|array|false', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], 'mb_convert_kana' => ['string', 'str'=>'string', 'option='=>'string', 'encoding='=>'string'], 'mb_convert_variables' => ['string|false', 'to_encoding'=>'string', 'from_encoding'=>'array|string', '&rw_vars'=>'string|array|object', '&...rw_vars='=>'string|array|object'], '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', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], -'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'], +'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], +'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable(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'], @@ -6338,33 +6340,33 @@ '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'], -'mb_preferred_mime_name' => ['string', 'encoding'=>'string'], +'mb_preferred_mime_name' => ['string|false', 'encoding'=>'string'], 'mb_regex_encoding' => ['string|bool', 'encoding='=>'string'], 'mb_regex_set_options' => ['string', 'options='=>'string'], 'mb_scrub' => ['string', 'str'=>'string', 'enc='=>'string'], 'mb_send_mail' => ['bool', 'to'=>'string', 'subject'=>'string', 'message'=>'string', 'additional_headers='=>'string|array|null', 'additional_parameter='=>'string'], -'mb_split' => ['array', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], -'mb_strcut' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'int', 'encoding='=>'string'], +'mb_split' => ['list|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], +'mb_strcut' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], 'mb_strimwidth' => ['string', 'str'=>'string', 'start'=>'int', 'width'=>'int', 'trimmarker='=>'string', 'encoding='=>'string'], -'mb_stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strlen' => ['int|false', 'str'=>'string', 'encoding='=>'string'], -'mb_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strlen' => ['0|positive-int|false', 'str'=>'string', 'encoding='=>'string'], +'mb_strpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], 'mb_strrichr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], -'mb_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strtolower' => ['string', 'str'=>'string', 'encoding='=>'string'], -'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], -'mb_strwidth' => ['int', 'str'=>'string', 'encoding='=>'string'], +'mb_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'], -'mb_substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'encoding='=>'string'], +'mb_substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'encoding='=>'string'], 'mcrypt_cbc' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], 'mcrypt_cfb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], 'mcrypt_create_iv' => ['string', 'size'=>'int', 'source='=>'int'], @@ -6398,11 +6400,11 @@ 'mcrypt_module_is_block_algorithm' => ['bool', 'algorithm'=>'string', 'lib_dir='=>'string'], 'mcrypt_module_is_block_algorithm_mode' => ['bool', 'mode'=>'string', 'lib_dir='=>'string'], 'mcrypt_module_is_block_mode' => ['bool', 'mode'=>'string', 'lib_dir='=>'string'], -'mcrypt_module_open' => ['resource', 'cipher'=>'string', 'cipher_directory'=>'string', 'mode'=>'string', 'mode_directory'=>'string'], +'mcrypt_module_open' => ['resource|false', 'cipher'=>'string', 'cipher_directory'=>'string', 'mode'=>'string', 'mode_directory'=>'string'], 'mcrypt_module_self_test' => ['bool', 'algorithm'=>'string', 'lib_dir='=>'string'], 'mcrypt_ofb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], -'md5' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'md5_file' => ['string|false', 'filename'=>'string', 'raw_output='=>'bool'], +'md5' => ['non-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'], @@ -6411,7 +6413,8 @@ 'Memcache::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'Memcache::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'Memcache::flush' => ['bool'], -'Memcache::get' => ['array', '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'], @@ -6435,7 +6438,7 @@ 'Memcached::decrementByKey' => ['int|false', 'server_key'=>'string', 'key'=>'string', 'offset='=>'int', 'initial_value='=>'int', 'expiry='=>'int'], 'Memcached::delete' => ['bool', 'key'=>'string', 'time='=>'int'], 'Memcached::deleteByKey' => ['bool', 'server_key'=>'string', 'key'=>'string', 'time='=>'int'], -'Memcached::deleteMulti' => ['bool', 'keys'=>'array', 'time='=>'int'], +'Memcached::deleteMulti' => ['array', 'keys'=>'array', 'time='=>'int'], 'Memcached::deleteMultiByKey' => ['bool', 'server_key'=>'string', 'keys'=>'array', 'time='=>'int'], 'Memcached::fetch' => ['array'], 'Memcached::fetchAll' => ['array'], @@ -6478,7 +6481,8 @@ 'MemcachePool::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'MemcachePool::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'MemcachePool::flush' => ['bool'], -'MemcachePool::get' => ['array', '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'], @@ -6488,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'], @@ -6503,14 +6507,14 @@ 'MessageFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'metaphone' => ['string', 'text'=>'string', 'phones='=>'int'], 'method_exists' => ['bool', 'object'=>'object|string', 'method'=>'string'], -'mhash' => ['string', 'hash'=>'int', 'data'=>'string', 'key='=>'string'], +'mhash' => ['string|false', 'hash'=>'int', 'data'=>'string', 'key='=>'string'], 'mhash_count' => ['int'], -'mhash_get_block_size' => ['int', 'hash'=>'int'], -'mhash_get_hash_name' => ['string', 'hash'=>'int'], -'mhash_keygen_s2k' => ['string', 'hash'=>'int', 'input_password'=>'string', 'salt'=>'string', 'bytes'=>'int'], +'mhash_get_block_size' => ['int|false', 'hash'=>'int'], +'mhash_get_hash_name' => ['string|false', 'hash'=>'int'], +'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'], -'min' => ['', '...arg1'=>'array'], +'mime_content_type' => ['string|false', 'filename_or_stream'=>'string|resource'], +'min' => ['', '...arg1'=>'non-empty-array'], 'min\'1' => ['', 'arg1'=>'', 'arg2'=>'', '...args='=>''], 'ming_keypress' => ['int', 'char'=>'string'], 'ming_setcubicthreshold' => ['void', 'threshold'=>'int'], @@ -6519,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', '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'], @@ -6579,7 +6583,7 @@ 'MongoCollection::aggregate\'1' => ['array', 'pipeline'=>'array', 'options='=>'array'], 'MongoCollection::aggregateCursor' => ['MongoCommandCursor', 'command'=>'array', 'options='=>'array'], 'MongoCollection::batchInsert' => ['mixed', 'a'=>'array', 'options='=>'array'], -'MongoCollection::count' => ['int', 'query='=>'array', 'limit='=>'int', 'skip='=>'int'], +'MongoCollection::count' => ['0|positive-int', 'query='=>'array', 'limit='=>'int', 'skip='=>'int'], 'MongoCollection::createDBRef' => ['array', 'a'=>'array'], 'MongoCollection::createIndex' => ['bool', 'keys'=>'array', 'options='=>'array'], 'MongoCollection::deleteIndex' => ['array', 'keys'=>'string|array'], @@ -6589,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'], @@ -6600,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'], @@ -6624,7 +6628,7 @@ 'MongoCursor::addOption' => ['MongoCursor', 'key'=>'string', 'value'=>'mixed'], 'MongoCursor::awaitData' => ['MongoCursor', 'wait='=>'bool'], 'MongoCursor::batchSize' => ['MongoCursor', 'num'=>'int'], -'MongoCursor::count' => ['int', 'foundonly='=>'bool'], +'MongoCursor::count' => ['0|positive-int', 'foundonly='=>'bool'], 'MongoCursor::current' => ['array'], 'MongoCursor::dead' => ['bool'], 'MongoCursor::doQuery' => ['void'], @@ -6662,7 +6666,7 @@ 'MongoCursorException::getLine' => ['int'], 'MongoCursorException::getMessage' => ['string'], 'MongoCursorException::getPrevious' => ['Exception|Throwable'], -'MongoCursorException::getTrace' => ['array'], +'MongoCursorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoCursorException::getTraceAsString' => ['string'], 'MongoCursorInterface::__construct' => ['void'], 'MongoCursorInterface::batchSize' => ['MongoCursorInterface', 'batchSize'=>'int'], @@ -6708,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\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'=>'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\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'], -'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'], -'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'], @@ -6841,7 +7080,7 @@ 'MongoException::getLine' => ['int'], 'MongoException::getMessage' => ['string'], 'MongoException::getPrevious' => ['Exception|Throwable'], -'MongoException::getTrace' => ['array'], +'MongoException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoException::getTraceAsString' => ['string'], 'MongoGridFS::__construct' => ['void', 'db'=>'MongoDB', 'prefix='=>'string', 'chunks='=>'mixed'], 'MongoGridFS::__get' => ['MongoCollection', 'name'=>'string'], @@ -6849,7 +7088,7 @@ 'MongoGridFS::aggregate' => ['array', 'pipeline'=>'array', 'op'=>'array', 'pipelineOperators'=>'array'], 'MongoGridFS::aggregateCursor' => ['MongoCommandCursor', 'pipeline'=>'array', 'options'=>'array'], 'MongoGridFS::batchInsert' => ['mixed', 'a'=>'array', 'options='=>'array'], -'MongoGridFS::count' => ['int', 'query='=>'stdClass|array'], +'MongoGridFS::count' => ['0|positive-int', 'query='=>'stdClass|array'], 'MongoGridFS::createDBRef' => ['array', 'a'=>'array'], 'MongoGridFS::createIndex' => ['array', 'keys'=>'array', 'options='=>'array'], 'MongoGridFS::delete' => ['bool', 'id'=>'mixed'], @@ -6884,7 +7123,7 @@ 'MongoGridFSCursor::addOption' => ['MongoCursor', 'key'=>'string', 'value'=>'mixed'], 'MongoGridFSCursor::awaitData' => ['MongoCursor', 'wait='=>'bool|true'], 'MongoGridFSCursor::batchSize' => ['MongoCursor', 'batchSize'=>'int'], -'MongoGridFSCursor::count' => ['int', 'all='=>'bool|false'], +'MongoGridFSCursor::count' => ['0|positive-int', 'all='=>'bool|false'], 'MongoGridFSCursor::current' => ['MongoGridFSFile'], 'MongoGridFSCursor::dead' => ['bool'], 'MongoGridFSCursor::doQuery' => ['void'], @@ -6934,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'], @@ -6952,7 +7191,7 @@ 'MongoResultException::getLine' => ['int'], 'MongoResultException::getMessage' => ['string'], 'MongoResultException::getPrevious' => ['Exception|Throwable'], -'MongoResultException::getTrace' => ['array'], +'MongoResultException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoResultException::getTraceAsString' => ['string'], 'MongoTimestamp::__construct' => ['void', 'sec='=>'int', 'inc='=>'int'], 'MongoTimestamp::__toString' => ['string'], @@ -6972,7 +7211,7 @@ 'MongoWriteConcernException::getLine' => ['int'], 'MongoWriteConcernException::getMessage' => ['string'], 'MongoWriteConcernException::getPrevious' => ['Exception|Throwable'], -'MongoWriteConcernException::getTrace' => ['array'], +'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'], @@ -7027,22 +7266,22 @@ 'msession_timeout' => ['int', 'session'=>'string', 'param='=>'int'], 'msession_uniq' => ['string', 'param'=>'int', 'classname='=>'string', 'data='=>'string'], 'msession_unlock' => ['int', 'session'=>'string', 'key'=>'int'], -'msg_get_queue' => ['resource', 'key'=>'int', 'perms='=>'int'], +'msg_get_queue' => ['resource|false', 'key'=>'int', 'perms='=>'int'], 'msg_queue_exists' => ['bool', 'key'=>'int'], 'msg_receive' => ['bool', 'queue'=>'resource', 'desiredmsgtype'=>'int', '&w_msgtype'=>'int', 'maxsize'=>'int', '&w_message'=>'mixed', 'unserialize='=>'bool', 'flags='=>'int', '&w_errorcode='=>'int'], 'msg_remove_queue' => ['bool', 'queue'=>'resource'], 'msg_send' => ['bool', 'queue'=>'resource', 'msgtype'=>'int', 'message'=>'mixed', 'serialize='=>'bool', 'blocking='=>'bool', '&w_errorcode='=>'int'], 'msg_set_queue' => ['bool', 'queue'=>'resource', 'data'=>'array'], -'msg_stat_queue' => ['array', 'queue'=>'resource'], +'msg_stat_queue' => ['array|false', 'queue'=>'resource'], 'msgfmt_create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], -'msgfmt_format' => ['string', 'fmt'=>'messageformatter', 'args'=>'array'], -'msgfmt_format_message' => ['string', 'locale'=>'string', 'pattern'=>'string', 'args'=>'array'], +'msgfmt_format' => ['string|false', 'fmt'=>'messageformatter', 'args'=>'array'], +'msgfmt_format_message' => ['string|false', 'locale'=>'string', 'pattern'=>'string', 'args'=>'array'], 'msgfmt_get_error_code' => ['int', 'fmt'=>'messageformatter'], 'msgfmt_get_error_message' => ['string', 'fmt'=>'messageformatter'], 'msgfmt_get_locale' => ['string', 'formatter'=>'messageformatter'], -'msgfmt_get_pattern' => ['string', 'fmt'=>'messageformatter'], -'msgfmt_parse' => ['array', 'fmt'=>'messageformatter', 'value'=>'string'], -'msgfmt_parse_message' => ['array', 'locale'=>'string', 'pattern'=>'string', 'source'=>'string'], +'msgfmt_get_pattern' => ['string|false', 'fmt'=>'messageformatter'], +'msgfmt_parse' => ['array|false', 'fmt'=>'messageformatter', 'value'=>'string'], +'msgfmt_parse_message' => ['array|false', 'locale'=>'string', 'pattern'=>'string', 'source'=>'string'], 'msgfmt_set_pattern' => ['bool', 'fmt'=>'messageformatter', 'pattern'=>'string'], 'msql_affected_rows' => ['int', 'result'=>'resource'], 'msql_close' => ['bool', 'link_identifier='=>'?resource'], @@ -7107,11 +7346,11 @@ 'mt_rand\'1' => ['int'], 'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'], 'MultipleIterator::__construct' => ['void', 'flags='=>'int'], -'MultipleIterator::attachIterator' => ['void', 'iterator'=>'iterator', 'infos='=>'string'], -'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'iterator'], +'MultipleIterator::attachIterator' => ['void', 'iterator'=>'Iterator', 'infos='=>'string'], +'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'Iterator'], 'MultipleIterator::countIterators' => ['int'], 'MultipleIterator::current' => ['array'], -'MultipleIterator::detachIterator' => ['void', 'iterator'=>'iterator'], +'MultipleIterator::detachIterator' => ['void', 'iterator'=>'Iterator'], 'MultipleIterator::getFlags' => ['int'], 'MultipleIterator::key' => ['array'], 'MultipleIterator::next' => ['void'], @@ -7179,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'], @@ -7190,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'], @@ -7210,16 +7449,16 @@ '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_begin_transaction' => ['bool', 'link'=>'mysqli', 'flags='=>'int', 'name='=>'string'], 'mysqli_change_user' => ['bool', 'link'=>'mysqli', 'user'=>'string', 'password'=>'string', 'database'=>'string'], 'mysqli_character_set_name' => ['string', 'link'=>'mysqli'], 'mysqli_close' => ['bool', 'link'=>'mysqli'], 'mysqli_commit' => ['bool', 'link'=>'mysqli', 'flags='=>'int', 'name='=>'string'], -'mysqli_connect' => ['mysqli|false', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string'], +'mysqli_connect' => ['mysqli|false|null', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string'], 'mysqli_connect_errno' => ['int'], -'mysqli_connect_error' => ['string'], +'mysqli_connect_error' => ['string|null'], 'mysqli_data_seek' => ['bool', 'result'=>'mysqli_result', 'offset'=>'int'], 'mysqli_debug' => ['bool', 'message'=>'string'], 'mysqli_disable_reads_from_master' => ['bool', 'link'=>'mysqli'], @@ -7232,16 +7471,17 @@ 'mysqli_enable_reads_from_master' => ['bool', 'link'=>'mysqli'], 'mysqli_enable_rpl_parse' => ['bool', 'link'=>'mysqli'], 'mysqli_errno' => ['int', 'link'=>'mysqli'], -'mysqli_error' => ['string', 'link'=>'mysqli'], +'mysqli_error' => ['string|null', 'link'=>'mysqli'], 'mysqli_error_list' => ['array', 'connection'=>'mysqli'], -'mysqli_fetch_all' => ['array', 'result'=>'mysqli_result', 'resulttype='=>'int'], -'mysqli_fetch_array' => ['array|null', '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|false', 'result'=>'mysqli_result'], +'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|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'], @@ -7249,18 +7489,18 @@ 'mysqli_free_result' => ['void', 'link'=>'mysqli_result'], 'mysqli_get_cache_stats' => ['array'], 'mysqli_get_charset' => ['object', 'link'=>'mysqli'], -'mysqli_get_client_info' => ['string', 'link'=>'mysqli'], +'mysqli_get_client_info' => ['string', 'link='=>'mysqli'], 'mysqli_get_client_stats' => ['array|false'], -'mysqli_get_client_version' => ['int', 'link'=>'mysqli'], +'mysqli_get_client_version' => ['int'], 'mysqli_get_connection_stats' => ['array|false', 'link'=>'mysqli'], 'mysqli_get_host_info' => ['string', 'link'=>'mysqli'], 'mysqli_get_links_stats' => ['array'], 'mysqli_get_proto_info' => ['int', 'link'=>'mysqli'], 'mysqli_get_server_info' => ['string', 'link'=>'mysqli'], 'mysqli_get_server_version' => ['int', 'link'=>'mysqli'], -'mysqli_get_warnings' => ['mysqli_warning', 'link'=>'mysqli'], +'mysqli_get_warnings' => ['mysqli_warning|false', 'link'=>'mysqli'], 'mysqli_info' => ['?string', 'link'=>'mysqli'], -'mysqli_init' => ['mysqli'], +'mysqli_init' => ['mysqli|false'], 'mysqli_insert_id' => ['int|string', 'link'=>'mysqli'], 'mysqli_kill' => ['bool', 'link'=>'mysqli', 'processid'=>'int'], 'mysqli_link_construct' => ['object'], @@ -7269,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'], @@ -7285,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|false'], +'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'], @@ -7319,19 +7560,19 @@ 'mysqli_stmt::close' => ['bool'], 'mysqli_stmt::data_seek' => ['void', 'offset'=>'int'], 'mysqli_stmt::execute' => ['bool'], -'mysqli_stmt::fetch' => ['bool'], +'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'], @@ -7340,24 +7581,24 @@ 'mysqli_stmt_data_seek' => ['void', 'stmt'=>'mysqli_stmt', 'offset'=>'int'], 'mysqli_stmt_errno' => ['int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_error' => ['string', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_error_list' => ['array', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_error_list' => ['list', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_execute' => ['bool', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_fetch' => ['bool', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_field_count' => ['int', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_fetch' => ['bool|null', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_field_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_free_result' => ['void', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_get_result' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_get_warnings' => ['object', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_init' => ['mysqli_stmt', 'link'=>'mysqli'], +'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'], 'mysqli_stmt_next_result' => ['bool', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_num_rows' => ['int', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_param_count' => ['int', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_num_rows' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_param_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_prepare' => ['bool', 'stmt'=>'mysqli_stmt', 'query'=>'string'], 'mysqli_stmt_reset' => ['bool', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_result_metadata' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_send_long_data' => ['bool', 'stmt'=>'mysqli_stmt', 'param_nr'=>'int', 'data'=>'string'], -'mysqli_stmt_sqlstate' => ['string', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_sqlstate' => ['non-empty-string', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_store_result' => ['bool', 'stmt'=>'mysqli_stmt'], 'mysqli_store_result' => ['mysqli_result|false', 'link'=>'mysqli', 'option='=>'int'], 'mysqli_thread_id' => ['int', 'link'=>'mysqli'], @@ -7750,23 +7991,23 @@ 'newt_win_message' => ['void', 'title'=>'string', 'button_text'=>'string', 'format'=>'string', 'args='=>'mixed', '...args='=>'mixed'], 'newt_win_messagev' => ['void', 'title'=>'string', 'button_text'=>'string', 'format'=>'string', 'args'=>'array'], 'newt_win_ternary' => ['int', 'title'=>'string', 'button1_text'=>'string', 'button2_text'=>'string', 'button3_text'=>'string', 'format'=>'string', 'args='=>'mixed', '...args='=>'mixed'], -'next' => ['mixed', '&rw_array_arg'=>'array'], +'next' => ['mixed', '&rw_array_arg'=>'array|object'], 'ngettext' => ['string', 'msgid1'=>'string', 'msgid2'=>'string', 'n'=>'int'], 'nl2br' => ['string', 'str'=>'string', 'is_xhtml='=>'bool'], -'nl_langinfo' => ['string', 'item'=>'int'], -'NoRewindIterator::__construct' => ['void', 'it'=>'iterator'], +'nl_langinfo' => ['string|false', 'item'=>'int'], +'NoRewindIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'NoRewindIterator::current' => ['mixed'], -'NoRewindIterator::getInnerIterator' => ['iterator'], +'NoRewindIterator::getInnerIterator' => ['Iterator'], 'NoRewindIterator::key' => ['mixed'], 'NoRewindIterator::next' => ['void'], 'NoRewindIterator::rewind' => ['void'], 'NoRewindIterator::valid' => ['bool'], 'Normalizer::getRawDecomposition' => ['string|null', 'input'=>'string'], 'Normalizer::isNormalized' => ['bool', 'input'=>'string', 'form='=>'int'], -'Normalizer::normalize' => ['string', 'input'=>'string', 'form='=>'int'], +'Normalizer::normalize' => ['string|false', 'input'=>'string', 'form='=>'int'], 'normalizer_get_raw_decomposition' => ['string|null', 'input'=>'string'], 'normalizer_is_normalized' => ['bool', 'input'=>'string', 'form='=>'int'], -'normalizer_normalize' => ['string', 'input'=>'string', 'form='=>'int'], +'normalizer_normalize' => ['string|false', 'input'=>'string', 'form='=>'int'], 'notes_body' => ['array', 'server'=>'string', 'mailbox'=>'string', 'msg_number'=>'int'], 'notes_copy_db' => ['bool', 'from_database_name'=>'string', 'to_database_name'=>'string'], 'notes_create_db' => ['bool', 'database_name'=>'string'], @@ -7785,12 +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'], -'number_format\'1' => ['string', 'number'=>'float', 'num_decimal_places'=>'int', 'dec_separator'=>'string', 'thousands_separator'=>'string'], +'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'], @@ -7799,21 +8039,21 @@ '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'], 'NumberFormatter::setTextAttribute' => ['bool', 'attr'=>'int', 'value'=>'string'], 'numfmt_create' => ['NumberFormatter', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], -'numfmt_format' => ['string', 'fmt'=>'numberformatter', 'value='=>'float', 'type='=>'int'], +'numfmt_format' => ['string|false', 'fmt'=>'numberformatter', 'value='=>'float', 'type='=>'int'], 'numfmt_format_currency' => ['string|false', 'fmt'=>'numberformatter', 'value'=>'float', 'currency'=>'string'], -'numfmt_get_attribute' => ['int', 'fmt'=>'numberformatter', 'attr'=>'int'], +'numfmt_get_attribute' => ['int|false', 'fmt'=>'numberformatter', 'attr'=>'int'], 'numfmt_get_error_code' => ['int', 'fmt'=>'numberformatter'], 'numfmt_get_error_message' => ['string', 'fmt'=>'numberformatter'], -'numfmt_get_locale' => ['string', 'fmt'=>'numberformatter', 'type='=>'int'], -'numfmt_get_pattern' => ['string', 'fmt'=>'numberformatter'], -'numfmt_get_symbol' => ['string', 'fmt'=>'numberformatter', 'attr'=>'int'], -'numfmt_get_text_attribute' => ['string', 'fmt'=>'numberformatter', 'attr'=>'int'], +'numfmt_get_locale' => ['string|false', 'fmt'=>'numberformatter', 'type='=>'int'], +'numfmt_get_pattern' => ['string|false', 'fmt'=>'numberformatter'], +'numfmt_get_symbol' => ['string|false', 'fmt'=>'numberformatter', 'attr'=>'int'], +'numfmt_get_text_attribute' => ['string|false', 'fmt'=>'numberformatter', 'attr'=>'int'], 'numfmt_parse' => ['float|false', 'fmt'=>'numberformatter', 'value'=>'string', 'type='=>'int', '&rw_position='=>'int'], 'numfmt_parse_currency' => ['float|false', 'fmt'=>'numberformatter', 'value'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], 'numfmt_set_attribute' => ['bool', 'fmt'=>'numberformatter', 'attr'=>'int', 'value'=>'int'], @@ -7880,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'], @@ -7925,9 +8165,9 @@ 'oci_collection_append' => ['bool', 'value'=>'string'], 'oci_collection_assign' => ['bool', 'from'=>'OCI-Collection'], 'oci_collection_element_assign' => ['bool', 'index'=>'int', 'val'=>'string'], -'oci_collection_element_get' => ['string', 'ndx'=>'int'], -'oci_collection_max' => ['int'], -'oci_collection_size' => ['int'], +'oci_collection_element_get' => ['string|false', 'ndx'=>'int'], +'oci_collection_max' => ['int|false'], +'oci_collection_size' => ['int|false'], 'oci_collection_trim' => ['bool', 'num'=>'int'], 'oci_commit' => ['bool', 'connection'=>'resource'], 'oci_connect' => ['resource|false', 'user'=>'string', 'pass'=>'string', 'db='=>'string', 'charset='=>'string', 'session_mode='=>'int'], @@ -7958,27 +8198,27 @@ 'oci_lob_close' => ['bool'], 'oci_lob_copy' => ['bool', 'lob_to'=>'OCI-Lob', 'lob_from'=>'OCI-Lob', 'length='=>'int'], 'oci_lob_eof' => ['bool'], -'oci_lob_erase' => ['int', 'offset'=>'int', 'length'=>'int'], +'oci_lob_erase' => ['int|false', 'offset'=>'int', 'length'=>'int'], 'oci_lob_export' => ['bool', 'filename'=>'string', 'start'=>'int', 'length'=>'int'], 'oci_lob_flush' => ['bool', 'flag'=>'int'], 'oci_lob_import' => ['bool', 'filename'=>'string'], 'oci_lob_is_equal' => ['bool', 'lob1'=>'OCI-Lob', 'lob2'=>'OCI-Lob'], -'oci_lob_load' => ['string'], -'oci_lob_read' => ['string', 'length'=>'int'], +'oci_lob_load' => ['string|false'], +'oci_lob_read' => ['string|false', 'length'=>'int'], 'oci_lob_rewind' => ['bool'], 'oci_lob_save' => ['bool', 'data'=>'string', 'offset'=>'int'], 'oci_lob_seek' => ['bool', 'offset'=>'int', 'whence'=>'int'], -'oci_lob_size' => ['int'], -'oci_lob_tell' => ['int'], +'oci_lob_size' => ['int|false'], +'oci_lob_tell' => ['int|false'], 'oci_lob_truncate' => ['bool', 'length'=>'int'], -'oci_lob_write' => ['int', 'string'=>'string', 'length'=>'int'], +'oci_lob_write' => ['int|false', 'string'=>'string', 'length'=>'int'], 'oci_lob_write_temporary' => ['bool', 'var'=>'string', 'lob_type'=>'int'], 'oci_new_collection' => ['OCI-Collection|false', 'connection'=>'resource', 'tdo'=>'string', 'schema='=>'string'], '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'], @@ -7995,10 +8235,10 @@ 'oci_set_prefetch' => ['bool', 'stmt'=>'resource', 'prefetch_rows'=>'int'], 'oci_statement_type' => ['string|false', 'stmt'=>'resource'], 'oci_unregister_taf_callback' => ['bool', 'connection'=>'resource'], -'ocifetchinto' => ['int', 'stmt'=>'', '&w_output'=>'array', 'mode='=>'int'], +'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'], @@ -8006,24 +8246,24 @@ 'odbc_columnprivileges' => ['resource', 'connection_id'=>'resource', 'catalog'=>'string', 'schema'=>'string', 'table'=>'string', 'column'=>'string'], 'odbc_columns' => ['resource', 'connection_id'=>'resource', 'qualifier='=>'string', 'owner='=>'string', 'table_name='=>'string', 'column_name='=>'string'], 'odbc_commit' => ['bool', 'connection_id'=>'resource'], -'odbc_connect' => ['resource', 'dsn'=>'string', 'user'=>'string', 'password'=>'string', 'cursor_option='=>'int'], -'odbc_cursor' => ['string', 'result_id'=>'resource'], -'odbc_data_source' => ['array', 'connection_id'=>'resource', 'fetch_type'=>'int'], +'odbc_connect' => ['resource|false', 'dsn'=>'string', 'user'=>'string', 'password'=>'string', 'cursor_option='=>'int'], +'odbc_cursor' => ['string|false', 'result_id'=>'resource'], +'odbc_data_source' => ['array|false', 'connection_id'=>'resource', 'fetch_type'=>'int'], 'odbc_do' => ['resource', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'odbc_error' => ['string', 'connection_id='=>'resource'], 'odbc_errormsg' => ['string', 'connection_id='=>'resource'], -'odbc_exec' => ['resource', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], +'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'odbc_execute' => ['bool', 'result_id'=>'resource', 'parameters_array='=>'array'], -'odbc_fetch_array' => ['array', 'result'=>'resource', 'rownumber='=>'int'], -'odbc_fetch_into' => ['int', 'result_id'=>'resource', '&w_result_array'=>'array', 'rownumber='=>'int'], -'odbc_fetch_object' => ['object', 'result'=>'int', 'rownumber='=>'int'], +'odbc_fetch_array' => ['array|false', 'result'=>'resource', 'rownumber='=>'int'], +'odbc_fetch_into' => ['int|false', 'result_id'=>'resource', '&w_result_array'=>'array', 'rownumber='=>'int'], +'odbc_fetch_object' => ['object|false', 'result'=>'int', 'rownumber='=>'int'], 'odbc_fetch_row' => ['bool', 'result_id'=>'resource', 'row_number='=>'int'], -'odbc_field_len' => ['int', 'result_id'=>'resource', 'field_number'=>'int'], -'odbc_field_name' => ['string', 'result_id'=>'resource', 'field_number'=>'int'], -'odbc_field_num' => ['int', 'result_id'=>'resource', 'field_name'=>'string'], -'odbc_field_precision' => ['int', 'result_id'=>'resource', 'field_number'=>'int'], -'odbc_field_scale' => ['int', 'result_id'=>'resource', 'field_number'=>'int'], -'odbc_field_type' => ['string', 'result_id'=>'resource', 'field_number'=>'int'], +'odbc_field_len' => ['int|false', 'result_id'=>'resource', 'field_number'=>'int'], +'odbc_field_name' => ['string|false', 'result_id'=>'resource', 'field_number'=>'int'], +'odbc_field_num' => ['int|false', 'result_id'=>'resource', 'field_name'=>'string'], +'odbc_field_precision' => ['int|false', 'result_id'=>'resource', 'field_number'=>'int'], +'odbc_field_scale' => ['int|false', 'result_id'=>'resource', 'field_number'=>'int'], +'odbc_field_type' => ['string|false', 'result_id'=>'resource', 'field_number'=>'int'], 'odbc_foreignkeys' => ['resource', 'connection_id'=>'resource', 'pk_qualifier'=>'string', 'pk_owner'=>'string', 'pk_table'=>'string', 'fk_qualifier'=>'string', 'fk_owner'=>'string', 'fk_table'=>'string'], 'odbc_free_result' => ['bool', 'result_id'=>'resource'], 'odbc_gettypeinfo' => ['resource', 'connection_id'=>'resource', 'data_type='=>'int'], @@ -8032,12 +8272,12 @@ 'odbc_num_fields' => ['int', 'result_id'=>'resource'], 'odbc_num_rows' => ['int', 'result_id'=>'resource'], 'odbc_pconnect' => ['resource', 'dsn'=>'string', 'user'=>'string', 'password'=>'string', 'cursor_option='=>'int'], -'odbc_prepare' => ['resource', 'connection_id'=>'resource', 'query'=>'string'], +'odbc_prepare' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string'], 'odbc_primarykeys' => ['resource', 'connection_id'=>'resource', 'qualifier'=>'string', 'owner'=>'string', 'table'=>'string'], 'odbc_procedurecolumns' => ['resource', 'connection_id'=>'', 'qualifier'=>'string', 'owner'=>'string', 'proc'=>'string', 'column'=>'string'], 'odbc_procedures' => ['resource', 'connection_id'=>'', 'qualifier'=>'string', 'owner'=>'string', 'name'=>'string'], 'odbc_result' => ['mixed', 'result_id'=>'resource', 'field'=>'mixed'], -'odbc_result_all' => ['int', 'result_id'=>'resource', 'format='=>'string'], +'odbc_result_all' => ['int|false', 'result_id'=>'resource', 'format='=>'string'], 'odbc_rollback' => ['bool', 'connection_id'=>'resource'], 'odbc_setoption' => ['bool', 'result_id'=>'resource', 'which'=>'int', 'option'=>'int', 'value'=>'int'], 'odbc_specialcolumns' => ['resource', 'connection_id'=>'resource', 'type'=>'int', 'qualifier'=>'string', 'owner'=>'string', 'table'=>'string', 'scope'=>'int', 'nullable'=>'int'], @@ -8045,7 +8285,7 @@ 'odbc_tableprivileges' => ['resource', 'connection_id'=>'resource', 'qualifier'=>'string', 'owner'=>'string', 'name'=>'string'], 'odbc_tables' => ['resource', 'connection_id'=>'resource', 'qualifier='=>'string', 'owner='=>'string', 'name='=>'string', 'table_types='=>'string'], 'opcache_compile_file' => ['bool', 'file'=>'string'], -'opcache_get_configuration' => ['array'], +'opcache_get_configuration' => ['array|false'], 'opcache_get_status' => ['array|false', 'get_scripts='=>'bool'], 'opcache_invalidate' => ['bool', 'script'=>'string', 'force='=>'bool'], 'opcache_is_script_cached' => ['bool', 'script'=>'string'], @@ -8061,7 +8301,7 @@ 'openal_context_process' => ['bool', 'context'=>'resource'], 'openal_context_suspend' => ['bool', 'context'=>'resource'], 'openal_device_close' => ['bool', 'device'=>'resource'], -'openal_device_open' => ['resource', 'device_desc='=>'string'], +'openal_device_open' => ['resource|false', 'device_desc='=>'string'], 'openal_listener_get' => ['mixed', 'property'=>'int'], 'openal_listener_set' => ['bool', 'property'=>'int', 'setting'=>'mixed'], 'openal_source_create' => ['resource'], @@ -8088,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'], -'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'], @@ -8105,8 +8345,8 @@ 'openssl_pkcs7_sign' => ['bool', 'infile'=>'string', 'outfile'=>'string', 'signcert'=>'string|resource', 'privkey'=>'string|resource|array', 'headers'=>'array', 'flags='=>'int', 'extracerts='=>'string'], 'openssl_pkcs7_verify' => ['bool|int', 'filename'=>'string', 'flags'=>'int', 'outfilename='=>'string', 'cainfo='=>'array', 'extracerts='=>'string', 'content='=>'string', 'p7bfilename='=>'string'], 'openssl_pkey_derive' => ['string|false', 'pub_key'=>'resource', 'priv_key'=>'resource', 'keylen='=>'int'], -'openssl_pkey_export' => ['bool', 'key'=>'resource', '&w_out'=>'string', 'passphrase='=>'string', 'configargs='=>'array'], -'openssl_pkey_export_to_file' => ['bool', 'key'=>'resource|string|array', 'outfilename'=>'string', 'passphrase='=>'string', 'configargs='=>'array'], +'openssl_pkey_export' => ['bool', 'key'=>'resource', '&w_out'=>'string', 'passphrase='=>'string|null', 'configargs='=>'array'], +'openssl_pkey_export_to_file' => ['bool', 'key'=>'resource|string|array', 'outfilename'=>'string', 'passphrase='=>'string|null', 'configargs='=>'array'], 'openssl_pkey_free' => ['void', 'key'=>'resource'], 'openssl_pkey_get_details' => ['array|false', 'key'=>'resource'], 'openssl_pkey_get_private' => ['resource|false', 'key'=>'string', 'passphrase='=>'string'], @@ -8117,13 +8357,13 @@ 'openssl_public_decrypt' => ['bool', 'data'=>'string', '&w_decrypted'=>'string', 'key'=>'string|resource', 'padding='=>'int'], 'openssl_public_encrypt' => ['bool', 'data'=>'string', '&w_crypted'=>'string', 'key'=>'string|resource', 'padding='=>'int'], 'openssl_random_pseudo_bytes' => ['string|false', 'length'=>'int', '&w_crypto_strong='=>'bool'], -'openssl_seal' => ['int|false', 'data'=>'string', '&w_sealed_data'=>'string', '&w_env_keys'=>'array', 'pub_key_ids'=>'array', 'method='=>'string'], +'openssl_seal' => ['int|false', 'data'=>'string', '&w_sealed_data'=>'string', '&w_env_keys'=>'array', 'pub_key_ids'=>'array', 'method='=>'string', '&w_iv='=>'string'], 'openssl_sign' => ['bool', 'data'=>'string', '&w_signature'=>'string', 'priv_key_id'=>'resource|string', 'signature_alg='=>'int|string'], -'openssl_spki_export' => ['string|null', 'spkac'=>'string'], -'openssl_spki_export_challenge' => ['string|null', 'spkac'=>'string'], -'openssl_spki_new' => ['string|null', 'privkey'=>'resource', 'challenge'=>'string', 'algorithm='=>'int'], +'openssl_spki_export' => ['string|null|false', 'spkac'=>'string'], +'openssl_spki_export_challenge' => ['string|null|false', 'spkac'=>'string'], +'openssl_spki_new' => ['string|null|false', 'privkey'=>'resource', 'challenge'=>'string', 'algorithm='=>'int'], 'openssl_spki_verify' => ['bool', 'spkac'=>'string'], -'openssl_verify' => ['int', 'data'=>'string', 'signature'=>'string', 'pub_key_id'=>'resource|string', 'signature_alg='=>'int|string'], +'openssl_verify' => ['-1|0|1|false', 'data'=>'string', 'signature'=>'string', 'pub_key_id'=>'resource|string', 'signature_alg='=>'int|string'], 'openssl_x509_check_private_key' => ['bool', 'cert'=>'string|resource', 'key'=>'string|resource|array'], 'openssl_x509_checkpurpose' => ['bool|int', 'x509cert'=>'string|resource', 'purpose'=>'int', 'cainfo='=>'array', 'untrustedfile='=>'string'], 'openssl_x509_export' => ['bool', 'x509'=>'string|resource', '&w_output'=>'string', 'notext='=>'bool'], @@ -8132,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)'], @@ -8142,7 +8382,7 @@ 'OutOfBoundsException::getLine' => ['int'], 'OutOfBoundsException::getMessage' => ['string'], 'OutOfBoundsException::getPrevious' => ['Throwable|OutOfBoundsException|null'], -'OutOfBoundsException::getTrace' => ['array'], +'OutOfBoundsException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OutOfBoundsException::getTraceAsString' => ['string'], 'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?OutOfRangeException)'], @@ -8152,7 +8392,7 @@ 'OutOfRangeException::getLine' => ['int'], 'OutOfRangeException::getMessage' => ['string'], 'OutOfRangeException::getPrevious' => ['Throwable|OutOfRangeException|null'], -'OutOfRangeException::getTrace' => ['array'], +'OutOfRangeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OutOfRangeException::getTraceAsString' => ['string'], 'output_add_rewrite_var' => ['bool', 'name'=>'string', 'value'=>'string'], 'output_reset_rewrite_vars' => ['bool'], @@ -8164,12 +8404,12 @@ 'OverflowException::getLine' => ['int'], 'OverflowException::getMessage' => ['string'], 'OverflowException::getPrevious' => ['Throwable|OverflowException|null'], -'OverflowException::getTrace' => ['array'], +'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'], 'pack' => ['string', 'format'=>'string', '...args='=>'mixed'], -'ParentIterator::__construct' => ['void', 'it'=>'recursiveiterator'], +'ParentIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator'], 'ParentIterator::accept' => ['bool'], 'ParentIterator::getChildren' => ['ParentIterator'], 'ParentIterator::hasChildren' => ['bool'], @@ -8231,7 +8471,7 @@ 'parse_ini_file' => ['array|false', 'filename'=>'string', 'process_sections='=>'bool', 'scanner_mode='=>'int'], 'parse_ini_string' => ['array|false', 'ini_string'=>'string', 'process_sections='=>'bool', 'scanner_mode='=>'int'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result='=>'array'], -'parse_url' => ['mixed', 'url'=>'string', 'url_component='=>'int'], +'parse_url' => ['array|int|string|false|null', 'url'=>'string', 'url_component='=>'int'], 'ParseError::__clone' => ['void'], 'ParseError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?ParseError)'], 'ParseError::__toString' => ['string'], @@ -8240,14 +8480,14 @@ 'ParseError::getLine' => ['int'], 'ParseError::getMessage' => ['string'], 'ParseError::getPrevious' => ['Throwable|ParseError|null'], -'ParseError::getTrace' => ['array'], +'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'], @@ -8260,24 +8500,24 @@ 'pcntl_exec' => ['bool', 'path'=>'string', 'args='=>'array', 'envs='=>'array'], 'pcntl_fork' => ['int'], 'pcntl_get_last_error' => ['int'], -'pcntl_getpriority' => ['int', 'pid='=>'int', 'process_identifier='=>'int'], +'pcntl_getpriority' => ['int|false', 'pid='=>'int', 'process_identifier='=>'int'], 'pcntl_setpriority' => ['bool', 'priority'=>'int', 'pid='=>'int', 'process_identifier='=>'int'], 'pcntl_signal' => ['bool', 'signo'=>'int', 'handle'=>'callable|int', 'restart_syscalls='=>'bool'], 'pcntl_signal_dispatch' => ['bool'], 'pcntl_signal_get_handler' => ['int|string', 'signo'=>'int'], 'pcntl_sigprocmask' => ['bool', 'how'=>'int', 'set'=>'array', '&w_oldset='=>'array'], -'pcntl_sigtimedwait' => ['int', 'set'=>'array', '&w_siginfo='=>'array', 'seconds='=>'int', 'nanoseconds='=>'int'], -'pcntl_sigwaitinfo' => ['int', 'set'=>'array', '&w_siginfo='=>'array'], +'pcntl_sigtimedwait' => ['int|false', 'set'=>'array', '&w_siginfo='=>'array', 'seconds='=>'int', 'nanoseconds='=>'int'], +'pcntl_sigwaitinfo' => ['int|false', 'set'=>'array', '&w_siginfo='=>'array'], 'pcntl_strerror' => ['string', 'errno'=>'int'], 'pcntl_wait' => ['int', '&w_status'=>'int', 'options='=>'int', '&w_rusage='=>'array'], 'pcntl_waitpid' => ['int', 'pid'=>'int', '&w_status'=>'int', 'options='=>'int', '&w_rusage='=>'array'], -'pcntl_wexitstatus' => ['int', 'status'=>'int'], +'pcntl_wexitstatus' => ['int|false', 'status'=>'int'], 'pcntl_wifcontinued' => ['bool', 'status'=>'int'], 'pcntl_wifexited' => ['bool', 'status'=>'int'], 'pcntl_wifsignaled' => ['bool', 'status'=>'int'], 'pcntl_wifstopped' => ['bool', 'status'=>'int'], -'pcntl_wstopsig' => ['int', 'status'=>'int'], -'pcntl_wtermsig' => ['int', 'status'=>'int'], +'pcntl_wstopsig' => ['int|false', 'status'=>'int'], +'pcntl_wtermsig' => ['int|false', 'status'=>'int'], 'PDF_activate_item' => ['bool', 'pdfdoc'=>'resource', 'id'=>'int'], 'PDF_add_launchlink' => ['bool', 'pdfdoc'=>'resource', 'llx'=>'float', 'lly'=>'float', 'urx'=>'float', 'ury'=>'float', 'filename'=>'string'], 'PDF_add_locallink' => ['bool', 'pdfdoc'=>'resource', 'lowerleftx'=>'float', 'lowerlefty'=>'float', 'upperrightx'=>'float', 'upperrighty'=>'float', 'page'=>'int', 'dest'=>'string'], @@ -8433,28 +8673,28 @@ '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'], 'PDO::cubrid_schema' => ['array', 'schema_type'=>'int', 'table_name='=>'string', 'col_name='=>'string'], 'PDO::errorCode' => ['string'], 'PDO::errorInfo' => ['array'], -'PDO::exec' => ['int', 'query'=>'string'], +'PDO::exec' => ['int|false', 'query'=>'string'], 'PDO::getAttribute' => ['', 'attribute'=>'int'], 'PDO::getAvailableDrivers' => ['array'], 'PDO::inTransaction' => ['bool'], -'PDO::lastInsertId' => ['string', 'seqname='=>'string'], -'PDO::pgsqlCopyFromArray' => ['bool', 'table_name'=>'string', 'rows'=>'array', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], -'PDO::pgsqlCopyFromFile' => ['bool', 'table_name'=>'string', 'filename'=>'string', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], -'PDO::pgsqlCopyToArray' => ['array', 'table_name'=>'string', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], -'PDO::pgsqlCopyToFile' => ['bool', 'table_name'=>'string', 'filename'=>'string', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], -'PDO::pgsqlGetNotify' => ['array', 'result_type'=>'int', 'ms_timeout'=>'int'], +'PDO::lastInsertId' => ['string|false', 'seqname='=>'string'], +'PDO::pgsqlCopyFromArray' => ['bool', 'table_name'=>'string', 'rows'=>'array', 'delimiter='=>'string', 'null_as='=>'string', 'fields='=>'string'], +'PDO::pgsqlCopyFromFile' => ['bool', 'table_name'=>'string', 'filename'=>'string', 'delimiter='=>'string', 'null_as='=>'string', 'fields='=>'string'], +'PDO::pgsqlCopyToArray' => ['array', 'table_name'=>'string', 'delimiter='=>'string', 'null_as='=>'string', 'fields='=>'string'], +'PDO::pgsqlCopyToFile' => ['bool', 'table_name'=>'string', 'filename'=>'string', 'delimiter='=>'string', 'null_as='=>'string', 'fields='=>'string'], +'PDO::pgsqlGetNotify' => ['array', 'result_type='=>'int', 'ms_timeout='=>'int'], 'PDO::pgsqlGetPid' => ['int'], 'PDO::pgsqlLOBCreate' => ['string'], 'PDO::pgsqlLOBOpen' => ['resource', 'oid'=>'string', 'mode='=>'string'], 'PDO::pgsqlLOBUnlink' => ['bool', 'oid'=>'string'], -'PDO::prepare' => ['PDOStatement', 'statement'=>'string', 'options='=>'array'], +'PDO::prepare' => ['__benevolent', 'statement'=>'string', 'options='=>'array'], 'PDO::query' => ['PDOStatement|false', 'sql'=>'string'], 'PDO::query\'1' => ['PDOStatement|false', 'sql'=>'string', 'fetch_column'=>'int', 'colno'=>'int'], 'PDO::query\'2' => ['PDOStatement|false', 'sql'=>'string', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs'=>'array'], @@ -8464,40 +8704,40 @@ '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' => [''], +'PDOException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'PDOException::getTraceAsString' => [''], -'PDOStatement::__sleep' => ['array'], +'PDOStatement::__sleep' => ['list'], 'PDOStatement::__wakeup' => ['void'], -'PDOStatement::bindColumn' => ['bool', 'column'=>'mixed', '&rw_param'=>'mixed', 'type='=>'int', 'maxlen='=>'int', 'driverdata='=>'mixed'], -'PDOStatement::bindParam' => ['bool', 'paramno'=>'mixed', '&rw_param'=>'mixed', 'type='=>'int', 'maxlen='=>'int', 'driverdata='=>'mixed'], -'PDOStatement::bindValue' => ['bool', 'paramno'=>'mixed', 'param'=>'mixed', 'type='=>'int'], +'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'], 'PDOStatement::execute' => ['bool', 'bound_input_params='=>'?array'], 'PDOStatement::fetch' => ['mixed', 'how='=>'int', 'orientation='=>'int', 'offset='=>'int'], 'PDOStatement::fetchAll' => ['array|false', 'how='=>'int', 'fetch_argument='=>'int|string|callable', 'ctor_args='=>'?array'], -'PDOStatement::fetchColumn' => ['string|null|false', 'column_number='=>'int'], +'PDOStatement::fetchColumn' => ['string|null|false|int', 'column_number='=>'int'], 'PDOStatement::fetchObject' => ['mixed', 'class_name='=>'string', 'ctor_args='=>'?array'], 'PDOStatement::getAttribute' => ['mixed', 'attribute'=>'int'], -'PDOStatement::getColumnMeta' => ['array', 'column'=>'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'], -'PDOStatement::setFetchMode\'2' => ['bool', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs'=>'array'], +'PDOStatement::setFetchMode\'2' => ['bool', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs='=>'?array'], 'PDOStatement::setFetchMode\'3' => ['bool', 'fetch_into'=>'int', 'object'=>'object'], -'pfsockopen' => ['resource', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], +'pfsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], 'pg_affected_rows' => ['int', 'result'=>'resource'], 'pg_cancel_query' => ['bool', 'connection'=>'resource'], 'pg_client_encoding' => ['string', 'connection='=>'resource'], @@ -8508,36 +8748,36 @@ 'pg_connection_reset' => ['bool', 'connection'=>'resource'], 'pg_connection_status' => ['int', 'connection'=>'resource'], 'pg_consume_input' => ['bool', 'connection'=>'resource'], -'pg_convert' => ['array', 'db'=>'resource', 'table'=>'string', 'values'=>'array', 'options='=>'int'], +'pg_convert' => ['array|false', 'db'=>'resource', 'table'=>'string', 'values'=>'array', 'options='=>'int'], 'pg_copy_from' => ['bool', 'connection'=>'resource', 'table_name'=>'string', 'rows'=>'array', 'delimiter='=>'string', 'null_as='=>'string'], -'pg_copy_to' => ['array', 'connection'=>'resource', 'table_name'=>'string', 'delimiter='=>'string', 'null_as='=>'string'], +'pg_copy_to' => ['array|false', 'connection'=>'resource', 'table_name'=>'string', 'delimiter='=>'string', 'null_as='=>'string'], 'pg_dbname' => ['string', 'connection='=>'resource'], 'pg_delete' => ['mixed', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int'], 'pg_end_copy' => ['bool', 'connection='=>'resource'], 'pg_escape_bytea' => ['string', 'connection'=>'resource', 'data'=>'string'], 'pg_escape_bytea\'1' => ['string', 'data'=>'string'], -'pg_escape_identifier' => ['string', 'connection'=>'resource', 'data'=>'string'], +'pg_escape_identifier' => ['string|false', 'connection'=>'resource', 'data'=>'string'], 'pg_escape_identifier\'1' => ['string', 'data'=>'string'], -'pg_escape_literal' => ['string', 'connection'=>'resource', '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_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_object' => ['object', 'result'=>'', 'row='=>'?int', 'result_type='=>'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', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], -'pg_field_is_null' => ['int', 'result'=>'', 'field_name_or_number'=>'string|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', 'result'=>'resource', 'field_number'=>'int'], +'pg_field_name' => ['string|false', 'result'=>'resource', 'field_number'=>'int'], 'pg_field_num' => ['int', 'result'=>'resource', 'field_name'=>'string'], -'pg_field_prtlen' => ['int', 'result'=>'', 'field_name_or_number'=>''], +'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_field_size' => ['int', 'result'=>'resource', 'field_number'=>'int'], 'pg_field_table' => ['mixed', 'result'=>'resource', 'field_number'=>'int', 'oid_only='=>'bool'], @@ -8545,35 +8785,35 @@ 'pg_field_type_oid' => ['int|false', 'result'=>'resource', 'field_number'=>'int'], 'pg_flush' => ['mixed', 'connection'=>'resource'], 'pg_free_result' => ['bool', 'result'=>'resource'], -'pg_get_notify' => ['array', 'connection'=>'resource', 'result_type='=>'int'], +'pg_get_notify' => ['array|false', 'connection'=>'resource', 'result_type='=>'int'], 'pg_get_pid' => ['int|false', 'connection'=>'resource'], 'pg_get_result' => ['resource|false', 'connection='=>'resource'], 'pg_host' => ['string', 'connection='=>'resource'], 'pg_insert' => ['mixed', 'db'=>'resource', 'table'=>'string', 'values'=>'array', 'options='=>'int'], 'pg_last_error' => ['string', 'connection='=>'resource', 'operation='=>'int'], 'pg_last_notice' => ['string', 'connection'=>'resource', 'option='=>'int'], -'pg_last_oid' => ['string', 'result'=>'resource'], +'pg_last_oid' => ['string|false', 'result'=>'resource'], 'pg_lo_close' => ['bool', 'large_object'=>'resource'], -'pg_lo_create' => ['int', 'connection='=>'resource', 'large_object_oid='=>''], +'pg_lo_create' => ['int|false', 'connection='=>'resource', 'large_object_oid='=>''], 'pg_lo_export' => ['bool', 'connection'=>'resource', 'oid'=>'int', 'filename'=>'string'], 'pg_lo_export\'1' => ['bool', 'oid'=>'int', 'pathname'=>'string'], -'pg_lo_import' => ['int', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], +'pg_lo_import' => ['int|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''], 'pg_lo_import\'1' => ['int', 'pathname'=>'string', 'oid'=>''], 'pg_lo_open' => ['resource|false', 'connection'=>'resource', 'oid'=>'int', 'mode'=>'string'], -'pg_lo_read' => ['string', 'large_object'=>'resource', 'len='=>'int'], +'pg_lo_read' => ['string|false', 'large_object'=>'resource', 'len='=>'int'], 'pg_lo_read_all' => ['int', 'large_object'=>'resource'], 'pg_lo_seek' => ['bool', 'large_object'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'pg_lo_tell' => ['int', 'large_object'=>'resource'], 'pg_lo_truncate' => ['bool', 'large_object'=>'resource', 'size'=>'int'], 'pg_lo_unlink' => ['bool', 'connection'=>'resource', 'oid'=>'int'], -'pg_lo_write' => ['int', 'large_object'=>'resource', 'data'=>'string', 'len='=>'int'], -'pg_meta_data' => ['array', 'db'=>'resource', 'table'=>'string', 'extended='=>'bool'], +'pg_lo_write' => ['int|false', 'large_object'=>'resource', 'data'=>'string', 'len='=>'int'], +'pg_meta_data' => ['array|false', 'db'=>'resource', 'table'=>'string', 'extended='=>'bool'], 'pg_num_fields' => ['int', 'result'=>'resource'], 'pg_num_rows' => ['int', 'result'=>'resource'], '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'], @@ -8595,7 +8835,7 @@ 'pg_send_query_params' => ['bool', 'connection'=>'resource', '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', 'connection'=>'resource', 'verbosity'=>'int'], +'pg_set_error_verbosity' => ['int|false', 'connection'=>'resource', 'verbosity'=>'int'], 'pg_set_error_verbosity\'1' => ['int', 'verbosity'=>'int'], 'pg_socket' => ['resource|false', 'connection'=>'resource'], 'pg_trace' => ['bool', 'filename'=>'string', 'mode='=>'string', 'connection='=>'resource'], @@ -8613,7 +8853,7 @@ 'Phar::addFromString' => ['', 'localname'=>'string', 'contents'=>'string'], 'Phar::apiVersion' => ['string'], 'Phar::buildFromDirectory' => ['array', 'base_dir'=>'string', 'regex='=>'string'], -'Phar::buildFromIterator' => ['array', 'iter'=>'iterator', 'base_directory='=>'string'], +'Phar::buildFromIterator' => ['array', 'iter'=>'Iterator', 'base_directory='=>'string'], 'Phar::canCompress' => ['bool', 'method='=>'int'], 'Phar::canWrite' => ['bool'], 'Phar::compress' => ['Phar', 'compression'=>'int', 'extension='=>'string'], @@ -8623,13 +8863,13 @@ 'Phar::convertToData' => ['PharData', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'], 'Phar::convertToExecutable' => ['Phar', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'], 'Phar::copy' => ['bool', 'oldfile'=>'string', 'newfile'=>'string'], -'Phar::count' => ['int'], +'Phar::count' => ['0|positive-int'], 'Phar::createDefaultStub' => ['string', 'indexfile='=>'string', 'webindexfile='=>'string'], 'Phar::decompress' => ['Phar', 'extension='=>'string'], 'Phar::decompressFiles' => ['bool'], 'Phar::delete' => ['bool', 'entry'=>'string'], 'Phar::delMetadata' => ['bool'], -'Phar::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array', 'overwrite='=>'bool'], +'Phar::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array|null', 'overwrite='=>'bool'], 'Phar::getAlias' => ['string'], 'Phar::getMetadata' => ['mixed'], 'Phar::getModified' => ['bool'], @@ -8670,7 +8910,7 @@ 'PharData::addFile' => ['', 'file'=>'string', 'localname='=>'string'], 'PharData::addFromString' => ['bool', 'localname'=>'string', 'contents'=>'string'], 'PharData::buildFromDirectory' => ['array', 'base_dir'=>'string', 'regex='=>'string'], -'PharData::buildFromIterator' => ['array', 'iter'=>'iterator', 'base_directory='=>'string'], +'PharData::buildFromIterator' => ['array', 'iter'=>'Iterator', 'base_directory='=>'string'], 'PharData::compress' => ['PharData', 'compression'=>'int', 'extension='=>'string'], 'PharData::compressFiles' => ['bool', 'compression'=>'int'], 'PharData::convertToData' => ['PharData', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'], @@ -8680,7 +8920,7 @@ 'PharData::decompressFiles' => ['bool'], 'PharData::delete' => ['bool', 'entry'=>'string'], 'PharData::delMetadata' => ['bool'], -'PharData::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array', 'overwrite='=>'bool'], +'PharData::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array|null', 'overwrite='=>'bool'], 'PharData::isWritable' => ['bool'], 'PharData::offsetGet' => ['PharFileInfo', 'offset'=>'string'], 'PharData::offsetSet' => ['', 'offset'=>'string', 'value'=>'string'], @@ -8722,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'], +'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'], @@ -8742,7 +8982,8 @@ 'phpdbg_prompt' => ['', 'prompt'=>'string'], 'phpdbg_start_oplog' => [''], 'phpinfo' => ['bool', 'what='=>'int'], -'phpversion' => ['string|false', 'extension='=>'string'], +'phpversion' => ['string'], +'phpversion\'1' => ['string|false', 'extension'=>'string'], 'pht\AtomicInteger::__construct' => ['void', 'value='=>'int'], 'pht\AtomicInteger::dec' => ['void'], 'pht\AtomicInteger::get' => ['int'], @@ -8778,7 +9019,7 @@ 'pointObj::distanceToLine' => ['float', 'p1'=>'pointObj', 'p2'=>'pointObj'], 'pointObj::distanceToPoint' => ['float', 'poPoint'=>'pointObj'], 'pointObj::distanceToShape' => ['float', 'shape'=>'shapeObj'], -'pointObj::draw' => ['int', 'map'=>'MapObj', 'layer'=>'layerObj', 'img'=>'imageObj', 'class_index'=>'int', 'text'=>'string'], +'pointObj::draw' => ['int', 'map'=>'mapObj', 'layer'=>'layerObj', 'img'=>'imageObj', 'class_index'=>'int', 'text'=>'string'], 'pointObj::ms_newPointObj' => ['pointObj'], 'pointObj::project' => ['int', 'in'=>'projectionObj', 'out'=>'projectionObj'], 'pointObj::setXY' => ['int', 'x'=>'float', 'y'=>'float', 'm'=>'float'], @@ -8792,25 +9033,25 @@ 'popen' => ['resource|false', 'command'=>'string', 'mode'=>'string'], 'pos' => ['mixed', 'array_arg'=>'array'], 'posix_access' => ['bool', 'file'=>'string', 'mode='=>'int'], -'posix_ctermid' => ['string'], +'posix_ctermid' => ['string|false'], 'posix_errno' => ['int'], 'posix_get_last_error' => ['int'], -'posix_getcwd' => ['string'], +'posix_getcwd' => ['string|false'], 'posix_getegid' => ['int'], 'posix_geteuid' => ['int'], 'posix_getgid' => ['int'], -'posix_getgrgid' => ['array', 'gid'=>'int'], -'posix_getgrnam' => ['array|false', 'groupname'=>'string'], -'posix_getgroups' => ['array'], -'posix_getlogin' => ['string'], +'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', 'uid'=>'int'], -'posix_getrlimit' => ['array'], -'posix_getsid' => ['int', 'pid'=>'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'], 'posix_initgroups' => ['bool', 'name'=>'string', 'base_group_id'=>'int'], 'posix_isatty' => ['bool', 'fd'=>'resource|int'], @@ -8825,28 +9066,27 @@ 'posix_setsid' => ['int'], 'posix_setuid' => ['bool', 'uid'=>'int'], 'posix_strerror' => ['string', 'errno'=>'int'], -'posix_times' => ['array'], +'posix_times' => ['array|false'], 'posix_ttyname' => ['string|false', 'fd'=>'resource|int'], -'posix_uname' => ['array'], +'posix_uname' => ['array|false'], 'Postal\Expand::expand_address' => ['string[]', 'address'=>'string', 'options='=>'array'], 'Postal\Parser::parse_address' => ['array', 'address'=>'string', 'options='=>'array'], 'pow' => ['float|int', 'base'=>'int|float', 'exponent'=>'int|float'], -'preg_filter' => ['mixed', 'regex'=>'mixed', 'replace'=>'mixed', 'subject'=>'mixed', 'limit='=>'int', '&w_count='=>'int'], -'preg_grep' => ['array', 'regex'=>'string', 'input'=>'array', 'flags='=>'int'], +'preg_filter' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], +'preg_grep' => ['array|false', 'regex'=>'string', 'input'=>'array', 'flags='=>'int'], 'preg_last_error' => ['int'], -'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'string[]', 'flags='=>'int', 'offset='=>'int'], -'preg_match_all' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], +'preg_match' => ['0|1|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'string[]', 'flags='=>'int', 'offset='=>'int'], +'preg_match_all' => ['0|positive-int|false|null', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_quote' => ['string', 'str'=>'string', 'delim_char='=>'string'], 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], +'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['array|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], -'prev' => ['mixed', '&rw_array_arg'=>'array'], -'print' => ['int', 'arg'=>'string'], +'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', '...args='=>'string|int|float'], +'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'proc_close' => ['int', 'process'=>'resource'], -'proc_get_status' => ['array', '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'], 'proc_open' => ['resource|false', 'command'=>'string', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'], 'proc_terminate' => ['bool', 'process'=>'resource', 'signal='=>'int'], @@ -8950,7 +9190,7 @@ 'pspell_new_personal' => ['int|false', 'personal'=>'string', 'language'=>'string', 'spelling='=>'string', 'jargon='=>'string', 'encoding='=>'string', 'mode='=>'int'], 'pspell_save_wordlist' => ['bool', 'pspell'=>'int'], 'pspell_store_replacement' => ['bool', 'pspell'=>'int', 'misspell'=>'string', 'correct'=>'string'], -'pspell_suggest' => ['array', 'pspell'=>'int', 'word'=>'string'], +'pspell_suggest' => ['array|false', 'pspell'=>'int', 'word'=>'string'], 'putenv' => ['bool', 'setting'=>'string'], 'px_close' => ['bool', 'pxdoc'=>'resource'], 'px_create_fp' => ['bool', 'pxdoc'=>'resource', 'file'=>'resource', 'fielddesc'=>'array'], @@ -9032,9 +9272,9 @@ 'quoted_printable_encode' => ['string', 'str'=>'string'], 'quotemeta' => ['string', 'str'=>'string'], 'rad2deg' => ['float', 'number'=>'float'], -'radius_acct_open' => ['resource'], +'radius_acct_open' => ['resource|false'], 'radius_add_server' => ['bool', 'radius_handle'=>'resource', 'hostname'=>'string', 'port'=>'int', 'secret'=>'string', 'timeout'=>'int', 'max_tries'=>'int'], -'radius_auth_open' => ['resource'], +'radius_auth_open' => ['resource|false'], 'radius_close' => ['bool', 'radius_handle'=>'resource'], 'radius_config' => ['bool', 'radius_handle'=>'resource', 'file'=>'string'], 'radius_create_request' => ['bool', 'radius_handle'=>'resource', 'type'=>'int'], @@ -9062,9 +9302,9 @@ 'radius_strerror' => ['string', 'radius_handle'=>'resource'], 'rand' => ['int', 'min'=>'int', 'max'=>'int'], 'rand\'1' => ['int'], -'random_bytes' => ['string', 'length'=>'int'], +'random_bytes' => ['non-empty-string', 'length'=>'positive-int'], 'random_int' => ['int', 'min'=>'int', 'max'=>'int'], -'range' => ['array', 'low'=>'mixed', 'high'=>'mixed', 'step='=>'int|float'], +'range' => ['array', 'low'=>'int|float|string', 'high'=>'int|float|string', 'step='=>'int|float'], 'RangeException::__clone' => ['void'], 'RangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?RangeException)'], 'RangeException::__toString' => ['string'], @@ -9073,7 +9313,7 @@ 'RangeException::getLine' => ['int'], 'RangeException::getMessage' => ['string'], 'RangeException::getPrevious' => ['Throwable|RangeException|null'], -'RangeException::getTrace' => ['array'], +'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'], @@ -9116,16 +9356,16 @@ 'RarException::getLine' => ['int'], 'RarException::getMessage' => ['string'], 'RarException::getPrevious' => ['Exception|Throwable'], -'RarException::getTrace' => ['array'], +'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'], -'readfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], -'readgzfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'int'], +'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'], 'readline_add_history' => ['bool', 'prompt'=>'string'], 'readline_callback_handler_install' => ['bool', 'prompt'=>'string', 'callback'=>'callable'], @@ -9140,14 +9380,14 @@ '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'], 'recode_file' => ['bool', 'request'=>'string', 'input'=>'resource', 'output'=>'resource'], 'recode_string' => ['string', 'request'=>'string', 'str'=>'string'], 'rectObj::__construct' => ['void'], -'rectObj::draw' => ['int', 'map'=>'MapObj', 'layer'=>'layerObj', 'img'=>'imageObj', 'class_index'=>'int', 'text'=>'string'], +'rectObj::draw' => ['int', 'map'=>'mapObj', 'layer'=>'layerObj', 'img'=>'imageObj', 'class_index'=>'int', 'text'=>'string'], 'rectObj::fit' => ['float', 'width'=>'int', 'height'=>'int'], 'rectObj::ms_newRectObj' => ['rectObj'], 'rectObj::project' => ['int', 'in'=>'projectionObj', 'out'=>'projectionObj'], @@ -9156,7 +9396,7 @@ 'RecursiveArrayIterator::__construct' => ['void', 'array='=>'array|object', 'flags='=>'int'], 'RecursiveArrayIterator::append' => ['void', 'value'=>'mixed'], 'RecursiveArrayIterator::asort' => ['void'], -'RecursiveArrayIterator::count' => ['int'], +'RecursiveArrayIterator::count' => ['0|positive-int'], 'RecursiveArrayIterator::current' => ['mixed'], 'RecursiveArrayIterator::getArrayCopy' => ['array'], 'RecursiveArrayIterator::getChildren' => ['RecursiveArrayIterator'], @@ -9175,14 +9415,14 @@ 'RecursiveArrayIterator::seek' => ['void', 'position'=>'int'], 'RecursiveArrayIterator::serialize' => ['string'], 'RecursiveArrayIterator::setFlags' => ['void', 'flags'=>'string'], -'RecursiveArrayIterator::uasort' => ['void', 'cmp_function'=>'callable(mixed,mixed):int'], -'RecursiveArrayIterator::uksort' => ['void', 'cmp_function'=>'callable(mixed,mixed):int'], +'RecursiveArrayIterator::uasort' => ['void', 'callback'=>'callable(mixed,mixed):int'], +'RecursiveArrayIterator::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'RecursiveArrayIterator::unserialize' => ['string', 'serialized'=>'string'], 'RecursiveArrayIterator::valid' => ['bool'], -'RecursiveCachingIterator::__construct' => ['void', 'it'=>'Iterator', 'flags'=>''], +'RecursiveCachingIterator::__construct' => ['void', 'iterator'=>'Iterator', 'flags'=>''], 'RecursiveCachingIterator::getChildren' => ['RecursiveCachingIterator'], 'RecursiveCachingIterator::hasChildren' => ['bool'], -'RecursiveCallbackFilterIterator::__construct' => ['void', 'it'=>'recursiveiterator', 'func'=>'callable'], +'RecursiveCallbackFilterIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator', 'func'=>'callable'], 'RecursiveCallbackFilterIterator::getChildren' => ['RecursiveCallbackFilterIterator'], 'RecursiveCallbackFilterIterator::hasChildren' => ['void'], 'RecursiveDirectoryIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], @@ -9193,12 +9433,12 @@ 'RecursiveDirectoryIterator::key' => ['string'], 'RecursiveDirectoryIterator::next' => ['void'], 'RecursiveDirectoryIterator::rewind' => ['void'], -'RecursiveFilterIterator::__construct' => ['void', 'it'=>'recursiveiterator'], +'RecursiveFilterIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator'], 'RecursiveFilterIterator::getChildren' => ['RecursiveFilterIterator'], 'RecursiveFilterIterator::hasChildren' => ['bool'], 'RecursiveIterator::getChildren' => ['RecursiveIterator'], 'RecursiveIterator::hasChildren' => ['bool'], -'RecursiveIteratorIterator::__construct' => ['void', 'it'=>'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'], @@ -9216,10 +9456,10 @@ 'RecursiveIteratorIterator::rewind' => ['void'], 'RecursiveIteratorIterator::setMaxDepth' => ['void', 'max_depth='=>'int'], 'RecursiveIteratorIterator::valid' => ['bool'], -'RecursiveRegexIterator::__construct' => ['void', 'it'=>'recursiveiterator', 'regex='=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], +'RecursiveRegexIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator', 'regex='=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], 'RecursiveRegexIterator::getChildren' => ['RecursiveRegexIterator'], 'RecursiveRegexIterator::hasChildren' => ['bool'], -'RecursiveTreeIterator::__construct' => ['void', 'it'=>'recursiveiterator|iteratoraggregate', 'flags='=>'int', 'cit_flags='=>'int', 'mode='=>'int'], +'RecursiveTreeIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'flags='=>'int', 'cit_flags='=>'int', 'mode='=>'int'], 'RecursiveTreeIterator::beginChildren' => ['void'], 'RecursiveTreeIterator::beginIteration' => ['RecursiveIterator'], 'RecursiveTreeIterator::callGetChildren' => ['RecursiveIterator'], @@ -9237,228 +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'], -'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' => ['bool|int', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::lPushx' => ['int', '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' => ['bool|int', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::rPushx' => ['int', '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', '&rw_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'=>'int'], -'Redis::setTimeout' => ['', 'key'=>'string', 'ttl'=>'int'], -'Redis::sGetMembers' => ['', '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::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' => ['__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' => ['int', 'key'=>'string'], -'Redis::subscribe' => ['', 'channels'=>'array', 'callback'=>'string'], +'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', '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'], -'RedisArray::__construct' => ['void', 'name='=>'string', 'hosts='=>'?array', 'opts='=>'?array'], +'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|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'], @@ -9481,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'], @@ -9503,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'], @@ -9571,30 +9859,30 @@ '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'], 'RedisCluster::sRandMember' => ['array|string', 'key'=>'string', 'count='=>'int'], 'RedisCluster::sRem' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], 'RedisCluster::sScan' => ['array', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'null', 'count='=>'int'], -'RedisCluster::strlen' => ['int', 'key'=>'string'], +'RedisCluster::strlen' => ['0|positive-int', 'key'=>'string'], 'RedisCluster::subscribe' => ['mixed', 'channels'=>'array', 'callback'=>'string'], 'RedisCluster::sUnion' => ['array', 'key1'=>'string', '...other_keys='=>'string'], 'RedisCluster::sUnionStore' => ['int', 'dstKey'=>'string', 'key1'=>'string', '...other_keys='=>'string'], -'RedisCluster::time' => ['array'], +'RedisCluster::time' => ['array', 'nodeParams'=>'string'], 'RedisCluster::ttl' => ['int', 'key'=>'string'], 'RedisCluster::type' => ['int', 'key'=>'string'], 'RedisCluster::unlink' => ['int', 'key'=>'string', '...other_keys='=>'string'], @@ -9644,35 +9932,35 @@ 'ReflectionClass::getConstant' => ['mixed', 'name'=>'string'], 'ReflectionClass::getConstants' => ['array'], 'ReflectionClass::getConstructor' => ['ReflectionMethod|null'], -'ReflectionClass::getDefaultProperties' => ['array'], +'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' => ['string[]'], +'ReflectionClass::getFileName' => ['non-empty-string|false'], +'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], 'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], -'ReflectionClass::getMethods' => ['ReflectionMethod[]', 'filter='=>'int'], +'ReflectionClass::getMethods' => ['list', 'filter='=>'int'], 'ReflectionClass::getModifiers' => ['int'], 'ReflectionClass::getName' => ['class-string'], 'ReflectionClass::getNamespaceName' => ['string'], 'ReflectionClass::getParentClass' => ['ReflectionClass|false'], -'ReflectionClass::getProperties' => ['ReflectionProperty[]', 'filter='=>'int'], +'ReflectionClass::getProperties' => ['list', 'filter='=>'int'], 'ReflectionClass::getProperty' => ['ReflectionProperty', 'name'=>'string'], 'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], -'ReflectionClass::getReflectionConstants' => ['array'], +'ReflectionClass::getReflectionConstants' => ['list'], 'ReflectionClass::getShortName' => ['string'], -'ReflectionClass::getStartLine' => ['int|false'], -'ReflectionClass::getStaticProperties' => ['ReflectionProperty[]'], +'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'], 'ReflectionClass::hasProperty' => ['bool', 'name'=>'string'], -'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'string|reflectionclass'], +'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'string|ReflectionClass'], 'ReflectionClass::inNamespace' => ['bool'], 'ReflectionClass::isAbstract' => ['bool'], 'ReflectionClass::isAnonymous' => ['bool'], @@ -9684,13 +9972,13 @@ 'ReflectionClass::isInternal' => ['bool'], 'ReflectionClass::isIterable' => ['bool'], 'ReflectionClass::isIterateable' => ['bool'], -'ReflectionClass::isSubclassOf' => ['bool', 'class'=>'string|reflectionclass'], +'ReflectionClass::isSubclassOf' => ['bool', 'class'=>'string|ReflectionClass'], 'ReflectionClass::isTrait' => ['bool'], 'ReflectionClass::isUserDefined' => ['bool'], 'ReflectionClass::newInstance' => ['object', 'args='=>'mixed', '...args='=>'mixed'], 'ReflectionClass::newInstanceArgs' => ['object', 'args='=>'array'], 'ReflectionClass::newInstanceWithoutConstructor' => ['object'], -'ReflectionClass::setStaticPropertyValue' => ['void', 'name'=>'string', 'value'=>'string'], +'ReflectionClass::setStaticPropertyValue' => ['void', 'name'=>'string', 'value'=>'mixed'], 'ReflectionClassConstant::__construct' => ['void', 'class'=>'mixed', 'name'=>'string'], 'ReflectionClassConstant::__toString' => ['string'], 'ReflectionClassConstant::export' => ['string', 'class'=>'mixed', 'name'=>'string', 'return='=>'bool'], @@ -9707,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'], @@ -9720,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'], @@ -9753,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'], @@ -9781,12 +10069,12 @@ 'ReflectionGenerator::getExecutingLine' => ['int'], 'ReflectionGenerator::getFunction' => ['ReflectionFunctionAbstract'], 'ReflectionGenerator::getThis' => ['object'], -'ReflectionGenerator::getTrace' => ['array', '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'], @@ -9818,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'], @@ -9836,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'], @@ -9860,7 +10148,7 @@ 'ReflectionZendExtension::getVersion' => ['string'], 'Reflector::__toString' => ['string'], 'Reflector::export' => ['?string'], -'RegexIterator::__construct' => ['void', 'it'=>'iterator', 'regex'=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], +'RegexIterator::__construct' => ['void', 'iterator'=>'Iterator', 'regex'=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], 'RegexIterator::accept' => ['bool'], 'RegexIterator::getFlags' => ['int'], 'RegexIterator::getMode' => ['int'], @@ -9870,35 +10158,35 @@ 'RegexIterator::setMode' => ['bool', 'new_mode'=>'int'], 'RegexIterator::setPregFlags' => ['bool', 'new_flags'=>'int'], 'register_event_handler' => ['bool', 'event_handler_func'=>'event_handler_func', 'handler_register_name'=>'handler_register_name', 'event_type_mask'=>'event_type_mask'], -'register_shutdown_function' => ['void', 'function'=>'callable(): void', '...parameter='=>'mixed'], +'register_shutdown_function' => ['void', 'function'=>'callable', '...parameter='=>'mixed'], 'register_tick_function' => ['bool', 'function'=>'callable(): void', '...args='=>'mixed'], 'rename' => ['bool', 'old_name'=>'string', 'new_name'=>'string', 'context='=>'resource'], 'rename_function' => ['bool', 'original_name'=>'string', 'new_name'=>'string'], -'reset' => ['mixed', '&rw_array_arg'=>'array'], +'reset' => ['mixed', '&rw_array'=>'array|object'], 'ResourceBundle::__construct' => ['void', 'locale'=>'string', 'bundlename'=>'string', 'fallback='=>'bool'], -'ResourceBundle::count' => ['int'], +'ResourceBundle::count' => ['0|positive-int'], 'ResourceBundle::create' => ['?ResourceBundle', 'locale'=>'string', 'bundlename'=>'string', 'fallback='=>'bool'], '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'], 'resourcebundle_get_error_code' => ['int', 'r'=>'resourcebundle'], 'resourcebundle_get_error_message' => ['string', 'r'=>'resourcebundle'], -'resourcebundle_locales' => ['array', 'bundlename'=>'string'], -'restore_error_handler' => ['bool'], -'restore_exception_handler' => ['bool'], +'resourcebundle_locales' => ['array|false', 'bundlename'=>'string'], +'restore_error_handler' => ['true'], +'restore_exception_handler' => ['true'], 'restore_include_path' => ['void'], 'rewind' => ['bool', 'fp'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'], -'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['__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'], -'rpm_open' => ['resource', 'filename'=>'string'], +'rpm_open' => ['resource|false', 'filename'=>'string'], 'rpm_version' => ['string'], 'rrd_create' => ['bool', 'filename'=>'string', 'options'=>'array'], 'rrd_error' => ['string'], @@ -9962,7 +10250,7 @@ 'RuntimeException::getLine' => ['int'], 'RuntimeException::getMessage' => ['string'], 'RuntimeException::getPrevious' => ['Throwable|RuntimeException|null'], -'RuntimeException::getTrace' => ['array'], +'RuntimeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RuntimeException::getTraceAsString' => ['string'], 'SAMConnection::commit' => ['bool'], 'SAMConnection::connect' => ['bool', 'protocol'=>'string', 'properties='=>'array'], @@ -9995,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'], @@ -10066,7 +10354,7 @@ 'SDO_Sequence::move' => ['void', 'toindex'=>'int', 'fromindex'=>'int'], 'SeekableIterator::seek' => ['void', 'position'=>'int'], 'sem_acquire' => ['bool', 'sem_identifier'=>'resource', 'nowait='=>'bool'], -'sem_get' => ['resource', 'key'=>'int', 'max_acquire='=>'int', 'perm='=>'int', 'auto_release='=>'int'], +'sem_get' => ['resource|false', 'key'=>'int', 'max_acquire='=>'int', 'perm='=>'int', 'auto_release='=>'int'], 'sem_release' => ['bool', 'sem_identifier'=>'resource'], 'sem_remove' => ['bool', 'sem_identifier'=>'resource'], 'Serializable::serialize' => ['string'], @@ -10087,19 +10375,19 @@ 'ServerResponse::setStatus' => ['void', 'status'=>'int'], 'ServerResponse::setVersion' => ['void', 'version'=>'string'], 'session_abort' => ['bool'], -'session_cache_expire' => ['int', 'new_cache_expire='=>'int'], -'session_cache_limiter' => ['string', 'new_cache_limiter='=>'string'], +'session_cache_expire' => ['int|false', 'new_cache_expire='=>'int'], +'session_cache_limiter' => ['string|false', 'new_cache_limiter='=>'string'], 'session_commit' => ['bool'], -'session_create_id' => ['string', 'prefix='=>'string'], +'session_create_id' => ['string|false', 'prefix='=>'string'], 'session_decode' => ['bool', 'data'=>'string'], 'session_destroy' => ['bool'], -'session_encode' => ['string'], -'session_gc' => ['int'], -'session_get_cookie_params' => ['array'], -'session_id' => ['string', 'newid='=>'string'], +'session_encode' => ['string|false'], +'session_gc' => ['int|false'], +'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', 'newname='=>'string'], -'session_name' => ['string', 'newname='=>'string'], +'session_module_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'], @@ -10110,20 +10398,20 @@ 'session_register' => ['bool', 'name'=>'mixed', '...args='=>'mixed'], 'session_register_shutdown' => ['void'], 'session_reset' => ['bool'], -'session_save_path' => ['string', 'newname='=>'string'], +'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'], 'SessionHandler::close' => ['bool'], 'SessionHandler::create_sid' => ['char'], 'SessionHandler::destroy' => ['bool', 'id'=>'string'], -'SessionHandler::gc' => ['bool', 'maxlifetime'=>'int'], +'SessionHandler::gc' => ['int|false', 'maxlifetime'=>'int'], 'SessionHandler::open' => ['bool', 'save_path'=>'string', 'session_name'=>'string'], 'SessionHandler::read' => ['string', 'id'=>'string'], 'SessionHandler::updateTimestamp' => ['bool', 'session_id'=>'string', 'session_data'=>'string'], @@ -10131,36 +10419,34 @@ 'SessionHandler::write' => ['bool', 'id'=>'string', 'data'=>'string'], 'SessionHandlerInterface::close' => ['bool'], 'SessionHandlerInterface::destroy' => ['bool', 'session_id'=>'string'], -'SessionHandlerInterface::gc' => ['bool', 'maxlifetime'=>'int'], +'SessionHandlerInterface::gc' => ['int|false', 'maxlifetime'=>'int'], 'SessionHandlerInterface::open' => ['bool', 'save_path'=>'string', 'name'=>'string'], -'SessionHandlerInterface::read' => ['string', 'session_id'=>'string'], +'SessionHandlerInterface::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'], 'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'], 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'val'=>'string'], 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'], -'set_error_handler' => ['?callable', 'error_handler'=>'null|callable(int,string,string,int,array):bool|callable(int,string,string,int):bool|callable(int,string,string):bool|callable(int,string):bool', 'error_types='=>'int'], +'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int,array):bool', 'error_types='=>'int'], 'set_exception_handler' => ['null|callable(Throwable):void', 'exception_handler'=>'null|callable(Throwable):void'], 'set_file_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], -'set_include_path' => ['string', 'new_include_path'=>'string'], +'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='=>'string', 'url_encode='=>'int'], -'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array'], +'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', '...args='=>'string'], +'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'], +'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'], @@ -10168,7 +10454,7 @@ 'shapefileObj::getExtent' => ['rectObj', 'i'=>'int'], 'shapefileObj::getPoint' => ['shapeObj', 'i'=>'int'], 'shapefileObj::getShape' => ['shapeObj', 'i'=>'int'], -'shapefileObj::getTransformed' => ['shapeObj', 'map'=>'MapObj', 'i'=>'int'], +'shapefileObj::getTransformed' => ['shapeObj', 'map'=>'mapObj', 'i'=>'int'], 'shapefileObj::ms_newShapefileObj' => ['shapefileObj', 'filename'=>'string', 'type'=>'int'], 'shapeObj::__construct' => ['void', 'type'=>'int'], 'shapeObj::add' => ['int', 'line'=>'lineObj'], @@ -10179,7 +10465,7 @@ 'shapeObj::crosses' => ['int', 'shape'=>'shapeObj'], 'shapeObj::difference' => ['shapeObj', 'shape'=>'shapeObj'], 'shapeObj::disjoint' => ['int', 'shape'=>'shapeObj'], -'shapeObj::draw' => ['int', 'map'=>'MapObj', 'layer'=>'layerObj', 'img'=>'imageObj'], +'shapeObj::draw' => ['int', 'map'=>'mapObj', 'layer'=>'layerObj', 'img'=>'imageObj'], 'shapeObj::equals' => ['int', 'shape'=>'shapeObj'], 'shapeObj::free' => ['void'], 'shapeObj::getArea' => ['float'], @@ -10203,8 +10489,8 @@ 'shapeObj::toWkt' => ['string'], 'shapeObj::union' => ['shapeObj', 'shape'=>'shapeObj'], 'shapeObj::within' => ['int', 'shape2'=>'shapeObj'], -'shell_exec' => ['?string', 'cmd'=>'string'], -'shm_attach' => ['resource', 'key'=>'int', 'memsize='=>'int', 'perm='=>'int'], +'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'], 'shm_has_var' => ['bool', 'shm_identifier'=>'resource', 'variable_key'=>'int'], @@ -10221,24 +10507,24 @@ 'shuffle' => ['bool', '&rw_array_arg'=>'array'], 'signeurlpaiement' => ['string', 'clent'=>'string', 'data'=>'string'], 'similar_text' => ['int', 'str1'=>'string', 'str2'=>'string', '&w_percent='=>'float'], -'simplexml_import_dom' => ['SimpleXMLElement|false', 'node'=>'DOMNode', 'class_name='=>'string'], +'simplexml_import_dom' => ['SimpleXMLElement|null', 'node'=>'DOMNode', 'class_name='=>'string'], 'simplexml_load_file' => ['SimpleXMLElement|false', 'filename'=>'string', 'class_name='=>'string', 'options='=>'int', 'ns='=>'string', 'is_prefix='=>'bool'], 'simplexml_load_string' => ['SimpleXMLElement|false', 'data'=>'string', 'class_name='=>'string', 'options='=>'int', 'ns='=>'string', 'is_prefix='=>'bool'], 'SimpleXMLElement::__construct' => ['void', 'data'=>'string', 'options='=>'int', 'data_is_url='=>'bool', 'ns='=>'string', 'is_prefix='=>'bool'], -'SimpleXMLElement::__get' => ['SimpleXMLElement', 'name'=>'string'], +'SimpleXMLElement::__get' => ['static', 'name'=>'string'], 'SimpleXMLElement::__toString' => ['string'], 'SimpleXMLElement::addAttribute' => ['void', 'name'=>'string', 'value='=>'string', 'ns='=>'string'], -'SimpleXMLElement::addChild' => ['SimpleXMLElement', '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' => ['SimpleXMLElement|null', 'ns='=>'string', 'is_prefix='=>'bool'], -'SimpleXMLElement::children' => ['SimpleXMLElement', 'ns='=>'string', 'is_prefix='=>'bool'], -'SimpleXMLElement::count' => ['int'], -'SimpleXMLElement::getDocNamespaces' => ['string[]', 'recursive='=>'bool', 'from_root='=>'bool'], +'SimpleXMLElement::attributes' => ['__benevolent', 'ns='=>'string', 'is_prefix='=>'bool'], +'SimpleXMLElement::children' => ['__benevolent', 'namespaceOrPrefix='=>'string|null', 'is_prefix='=>'bool'], +'SimpleXMLElement::count' => ['0|positive-int'], +'SimpleXMLElement::getDocNamespaces' => ['string[]|false', 'recursive='=>'bool', 'from_root='=>'bool'], 'SimpleXMLElement::getName' => ['string'], 'SimpleXMLElement::getNamespaces' => ['string[]', 'recursive='=>'bool'], 'SimpleXMLElement::registerXPathNamespace' => ['bool', 'prefix'=>'string', 'ns'=>'string'], -'SimpleXMLElement::xpath' => ['SimpleXMLElement[]|false', 'path'=>'string'], -'SimpleXMLIterator::current' => ['SimpleXMLIterator|null'], +'SimpleXMLElement::xpath' => ['static[]|false|null', 'path'=>'string'], +'SimpleXMLIterator::current' => ['SimpleXMLIterator'], 'SimpleXMLIterator::getChildren' => ['SimpleXMLIterator'], 'SimpleXMLIterator::hasChildren' => ['bool'], 'SimpleXMLIterator::key' => ['string|false'], @@ -10282,24 +10568,24 @@ 'snmpset' => ['bool', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'type'=>'string', 'value'=>'mixed', 'timeout='=>'int', 'retries='=>'int'], 'snmpwalk' => ['array|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], 'snmpwalkoid' => ['array|false', 'hostname'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'SoapClient::__call' => ['', 'function_name'=>'string', 'arguments'=>'array'], +'SoapClient::__call' => ['mixed', 'function_name'=>'string', 'arguments'=>'array'], 'SoapClient::__construct' => ['void', 'wsdl'=>'mixed', 'options='=>'array|null'], -'SoapClient::__doRequest' => ['string', 'request'=>'string', 'location'=>'string', 'action'=>'string', 'version'=>'int', 'one_way='=>'int'], +'SoapClient::__doRequest' => ['string|null', 'request'=>'string', 'location'=>'string', 'action'=>'string', 'version'=>'int', 'one_way='=>'int'], 'SoapClient::__getCookies' => ['array'], -'SoapClient::__getFunctions' => ['array'], -'SoapClient::__getLastRequest' => ['string'], -'SoapClient::__getLastRequestHeaders' => ['string'], -'SoapClient::__getLastResponse' => ['string'], -'SoapClient::__getLastResponseHeaders' => ['string'], -'SoapClient::__getTypes' => ['array'], +'SoapClient::__getFunctions' => ['array|null'], +'SoapClient::__getLastRequest' => ['string|null'], +'SoapClient::__getLastRequestHeaders' => ['string|null'], +'SoapClient::__getLastResponse' => ['string|null'], +'SoapClient::__getLastResponseHeaders' => ['string|null'], +'SoapClient::__getTypes' => ['array|null'], 'SoapClient::__setCookie' => ['', 'name'=>'string', 'value='=>'string'], -'SoapClient::__setLocation' => ['string', 'new_location='=>'string'], +'SoapClient::__setLocation' => ['string|null', 'new_location='=>'string'], 'SoapClient::__setSoapHeaders' => ['bool', 'soapheaders='=>''], -'SoapClient::__soapCall' => ['', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'SoapHeader|array', '&w_output_headers='=>'array'], +'SoapClient::__soapCall' => ['mixed', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'SoapHeader|array', '&w_output_headers='=>'array'], 'SoapClient::SoapClient' => ['object', 'wsdl'=>'mixed', 'options='=>'array|null'], -'SoapFault::__construct' => ['void', 'faultcode'=>'string', 'faultstring'=>'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', 'faultstring'=>'string', 'faultactor='=>'string', 'detail='=>'string', 'faultname='=>'string', 'headerfault='=>'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'], 'SoapHeader::SoapHeader' => ['object', 'namespace'=>'string', 'name'=>'string', 'data='=>'mixed', 'mustunderstand='=>'bool', 'actor='=>'string'], 'SoapParam::__construct' => ['void', 'data'=>'mixed', 'name'=>'string'], @@ -10317,10 +10603,10 @@ 'SoapVar::__construct' => ['void', 'data'=>'mixed', 'encoding'=>'int', 'type_name='=>'string|null', 'type_namespace='=>'string|null', 'node_name='=>'string|null', 'node_namespace='=>'string|null'], 'SoapVar::SoapVar' => ['object', 'data'=>'mixed', 'encoding'=>'int', 'type_name='=>'string|null', 'type_namespace='=>'string|null', 'node_name='=>'string|null', 'node_namespace='=>'string|null'], 'socket_accept' => ['resource|false', 'socket'=>'resource'], -'socket_addrinfo_bind' => ['resource|null', 'addrinfo'=>'resource'], -'socket_addrinfo_connect' => ['resource|null', 'addrinfo'=>'resource'], +'socket_addrinfo_bind' => ['resource|null|false', 'addrinfo'=>'resource'], +'socket_addrinfo_connect' => ['resource|null|false', 'addrinfo'=>'resource'], 'socket_addrinfo_explain' => ['array', 'addrinfo'=>'resource'], -'socket_addrinfo_lookup' => ['resource[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], +'socket_addrinfo_lookup' => ['resource[]|false', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], 'socket_bind' => ['bool', 'socket'=>'resource', 'addr'=>'string', 'port='=>'int'], 'socket_clear_error' => ['void', 'socket='=>'resource'], 'socket_close' => ['void', 'socket'=>'resource'], @@ -10341,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', 'tv_usec='=>'int'], +'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'], @@ -10415,7 +10701,7 @@ 'Sodium\randombytes_uniform' => ['int', 'upperBoundNonInclusive'=>'int'], 'Sodium\version_string' => ['string'], 'sodium_add' => ['string', 'string_1'=>'string', 'string_2'=>'string'], -'sodium_base642bin' => ['string', 'base64'=>'string', 'variant'=>'int', 'ignore'=>'string'], +'sodium_base642bin' => ['string', 'base64'=>'string', 'variant'=>'int', 'ignore='=>'string'], 'sodium_bin2base64' => ['string', 'binary'=>'string', 'variant'=>'int'], 'sodium_bin2hex' => ['string', 'binary'=>'string'], 'sodium_compare' => ['int', 'string_1'=>'string', 'string_2'=>'string'], @@ -10423,7 +10709,7 @@ 'sodium_crypto_aead_aes256gcm_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_aead_aes256gcm_is_available' => ['bool'], 'sodium_crypto_aead_aes256gcm_keygen' => ['string'], -'sodium_crypto_aead_chacha20poly1305_decrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], +'sodium_crypto_aead_chacha20poly1305_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_aead_chacha20poly1305_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_aead_chacha20poly1305_ietf_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], @@ -10445,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'], @@ -10474,23 +10760,23 @@ 'sodium_crypto_secretstream_xchacha20poly1305_init_pull' => ['string', 'header'=>'string', 'key'=>'string'], 'sodium_crypto_secretstream_xchacha20poly1305_init_push' => ['array', 'key'=>'string'], 'sodium_crypto_secretstream_xchacha20poly1305_keygen' => ['string'], -'sodium_crypto_secretstream_xchacha20poly1305_pull' => ['array', 'state'=>'string', 'c'=>'string', 'ad='=>'string'], +'sodium_crypto_secretstream_xchacha20poly1305_pull' => ['array|false', 'state'=>'string', 'c'=>'string', 'ad='=>'string'], 'sodium_crypto_secretstream_xchacha20poly1305_push' => ['string', 'state'=>'string', 'msg'=>'string', 'ad='=>'string', 'tag='=>'int'], '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'], @@ -10818,7 +11104,7 @@ 'SolrException::getLine' => ['int'], 'SolrException::getMessage' => ['string'], 'SolrException::getPrevious' => ['Exception|Throwable'], -'SolrException::getTrace' => ['array'], +'SolrException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrException::getTraceAsString' => ['string'], 'SolrGenericResponse::__construct' => ['void'], 'SolrGenericResponse::__destruct' => [''], @@ -10843,7 +11129,7 @@ 'SolrIllegalArgumentException::getLine' => ['int'], 'SolrIllegalArgumentException::getMessage' => ['string'], 'SolrIllegalArgumentException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalArgumentException::getTrace' => ['array'], +'SolrIllegalArgumentException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalArgumentException::getTraceAsString' => ['string'], 'SolrIllegalOperationException::__clone' => ['void'], 'SolrIllegalOperationException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], @@ -10855,7 +11141,7 @@ 'SolrIllegalOperationException::getLine' => ['int'], 'SolrIllegalOperationException::getMessage' => ['string'], 'SolrIllegalOperationException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalOperationException::getTrace' => ['array'], +'SolrIllegalOperationException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalOperationException::getTraceAsString' => ['string'], 'SolrInputDocument::__clone' => ['void'], 'SolrInputDocument::__construct' => ['void'], @@ -11163,7 +11449,7 @@ 'SolrServerException::getLine' => ['int'], 'SolrServerException::getMessage' => ['string'], 'SolrServerException::getPrevious' => ['Exception|Throwable'], -'SolrServerException::getTrace' => ['array'], +'SolrServerException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrServerException::getTraceAsString' => ['string'], 'SolrUpdateResponse::__construct' => ['void'], 'SolrUpdateResponse::__destruct' => [''], @@ -11179,7 +11465,7 @@ 'SolrUpdateResponse::setParseMode' => ['bool', 'parser_mode='=>'int'], 'SolrUpdateResponse::success' => ['bool'], 'SolrUtils::digestXmlResponse' => ['SolrObject', 'xmlresponse'=>'string', 'parse_mode='=>'int'], -'SolrUtils::escapeQueryChars' => ['string', 'str'=>'string'], +'SolrUtils::escapeQueryChars' => ['string|false', 'str'=>'string'], 'SolrUtils::getSolrVersion' => ['string'], 'SolrUtils::queryPhrase' => ['string', 'str'=>'string'], 'sort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], @@ -11222,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'], @@ -11230,7 +11516,7 @@ 'spl_object_id' => ['int', 'obj'=>'object'], 'SplDoublyLinkedList::add' => ['void', 'index'=>'mixed', 'newval'=>'mixed'], 'SplDoublyLinkedList::bottom' => ['mixed'], -'SplDoublyLinkedList::count' => ['int'], +'SplDoublyLinkedList::count' => ['0|positive-int'], 'SplDoublyLinkedList::current' => ['mixed'], 'SplDoublyLinkedList::getIteratorMode' => ['int'], 'SplDoublyLinkedList::isEmpty' => ['bool'], @@ -11255,24 +11541,24 @@ 'SplEnum::getConstList' => ['array', 'include_default='=>'bool'], 'SplFileInfo::__construct' => ['void', 'file_name'=>'string'], 'SplFileInfo::__toString' => ['string'], -'SplFileInfo::getATime' => ['int'], +'SplFileInfo::getATime' => ['__benevolent'], 'SplFileInfo::getBasename' => ['string', 'suffix='=>'string'], 'SplFileInfo::getCTime' => ['int'], 'SplFileInfo::getExtension' => ['string'], 'SplFileInfo::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileInfo::getFilename' => ['string'], -'SplFileInfo::getGroup' => ['int'], -'SplFileInfo::getInode' => ['int'], -'SplFileInfo::getLinkTarget' => ['string'], -'SplFileInfo::getMTime' => ['int'], -'SplFileInfo::getOwner' => ['int'], +'SplFileInfo::getGroup' => ['__benevolent'], +'SplFileInfo::getInode' => ['__benevolent'], +'SplFileInfo::getLinkTarget' => ['__benevolent'], +'SplFileInfo::getMTime' => ['__benevolent'], +'SplFileInfo::getOwner' => ['__benevolent'], 'SplFileInfo::getPath' => ['string'], -'SplFileInfo::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], +'SplFileInfo::getPathInfo' => ['__benevolent', 'class_name='=>'string'], 'SplFileInfo::getPathname' => ['string'], -'SplFileInfo::getPerms' => ['int'], -'SplFileInfo::getRealPath' => ['string|false'], -'SplFileInfo::getSize' => ['int'], -'SplFileInfo::getType' => ['string'], +'SplFileInfo::getPerms' => ['__benevolent'], +'SplFileInfo::getRealPath' => ['__benevolent'], +'SplFileInfo::getSize' => ['__benevolent'], +'SplFileInfo::getType' => ['__benevolent'], 'SplFileInfo::isDir' => ['bool'], 'SplFileInfo::isExecutable' => ['bool'], 'SplFileInfo::isFile' => ['bool'], @@ -11288,22 +11574,23 @@ 'SplFileObject::eof' => ['bool'], 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], -'SplFileObject::fgetcsv' => ['array|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'SplFileObject::fgets' => ['string|false'], +// Do not believe https://www.php.net/manual/en/splfileobject.fgetcsv#refsect1-splfileobject.fgetcsv-returnvalues +'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'], 'SplFileObject::fscanf' => ['bool', 'format'=>'string', '&...w_vars='=>'string|int|float'], 'SplFileObject::fseek' => ['int', 'pos'=>'int', 'whence='=>'int'], -'SplFileObject::fstat' => ['array|false'], +'SplFileObject::fstat' => ['array'], 'SplFileObject::ftell' => ['int|false'], 'SplFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplFileObject::fwrite' => ['int', 'str'=>'string', 'length='=>'int'], 'SplFileObject::getChildren' => ['null'], 'SplFileObject::getCsvControl' => ['array'], -'SplFileObject::getCurrentLine' => ['string|false'], +'SplFileObject::getCurrentLine' => ['string'], 'SplFileObject::getFlags' => ['int'], 'SplFileObject::getMaxLineLen' => ['int'], 'SplFileObject::hasChildren' => ['false'], @@ -11317,7 +11604,7 @@ 'SplFileObject::valid' => ['bool'], 'SplFixedArray::__construct' => ['void', 'size='=>'int'], 'SplFixedArray::__wakeup' => ['void'], -'SplFixedArray::count' => ['int'], +'SplFixedArray::count' => ['0|positive-int'], 'SplFixedArray::current' => ['mixed'], 'SplFixedArray::fromArray' => ['SplFixedArray', 'data'=>'array', 'save_indexes='=>'bool'], 'SplFixedArray::getSize' => ['int'], @@ -11332,7 +11619,7 @@ 'SplFixedArray::toArray' => ['array'], 'SplFixedArray::valid' => ['bool'], 'SplHeap::compare' => ['int', 'value1'=>'mixed', 'value2'=>'mixed'], -'SplHeap::count' => ['int'], +'SplHeap::count' => ['0|positive-int'], 'SplHeap::current' => ['mixed'], 'SplHeap::extract' => ['mixed'], 'SplHeap::insert' => ['bool', 'value'=>'mixed'], @@ -11348,10 +11635,10 @@ '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' => ['int'], +'SplObjectStorage::count' => ['0|positive-int'], 'SplObjectStorage::current' => ['object'], 'SplObjectStorage::detach' => ['void', 'obj'=>'object'], 'SplObjectStorage::getHash' => ['string', 'obj'=>'object'], @@ -11362,16 +11649,16 @@ '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'], 'SplObjectStorage::unserialize' => ['void', 'serialized'=>'string'], 'SplObjectStorage::valid' => ['bool'], -'SplObserver::update' => ['void', 'subject'=>'splsubject'], +'SplObserver::update' => ['void', 'subject'=>'SplSubject'], 'SplPriorityQueue::compare' => ['int', 'a'=>'mixed', 'b'=>'mixed'], -'SplPriorityQueue::count' => ['int'], +'SplPriorityQueue::count' => ['0|positive-int'], 'SplPriorityQueue::current' => ['mixed'], 'SplPriorityQueue::extract' => ['mixed'], 'SplPriorityQueue::getExtractFlags' => ['int'], @@ -11388,8 +11675,8 @@ 'SplQueue::enqueue' => ['void', 'value'=>'mixed'], 'SplQueue::setIteratorMode' => ['void', 'mode'=>'int'], 'SplStack::setIteratorMode' => ['void', 'mode'=>'int'], -'SplSubject::attach' => ['void', 'observer'=>'splobserver'], -'SplSubject::detach' => ['void', 'observer'=>'splobserver'], +'SplSubject::attach' => ['void', 'observer'=>'SplObserver'], +'SplSubject::detach' => ['void', 'observer'=>'SplObserver'], 'SplSubject::notify' => ['void'], 'SplTempFileObject::__construct' => ['void', 'max_memory='=>'int'], 'SplType::__construct' => ['void', 'initial_value='=>'mixed', 'strict='=>'bool'], @@ -11399,7 +11686,7 @@ 'Spoofchecker::setAllowedLocales' => ['void', 'locale_list'=>'string'], 'Spoofchecker::setChecks' => ['void', 'checks'=>'long'], 'Spoofchecker::setRestrictionLevel' => ['void', 'restriction_level'=>'int'], -'sprintf' => ['string', 'format'=>'string', '...args='=>'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'], @@ -11416,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'], @@ -11433,7 +11720,7 @@ 'SQLite3Stmt::bindValue' => ['bool', 'parameter_name_or_number'=>'string|int', 'parameter'=>'mixed', 'type='=>'int'], 'SQLite3Stmt::clear' => ['bool'], 'SQLite3Stmt::close' => ['bool'], -'SQLite3Stmt::execute' => ['SQLite3Result'], +'SQLite3Stmt::execute' => ['false|SQLite3Result'], 'SQLite3Stmt::paramCount' => ['int'], 'SQLite3Stmt::readOnly' => ['bool'], 'SQLite3Stmt::reset' => ['bool'], @@ -11465,8 +11752,8 @@ 'sqlite_next' => ['bool', 'result'=>''], 'sqlite_num_fields' => ['int', 'result'=>''], 'sqlite_num_rows' => ['int', 'result'=>''], -'sqlite_open' => ['resource', 'filename'=>'string', 'mode='=>'int', 'error_message='=>'string'], -'sqlite_popen' => ['resource', 'filename'=>'string', 'mode='=>'int', 'error_message='=>'string'], +'sqlite_open' => ['resource|false', 'filename'=>'string', 'mode='=>'int', 'error_message='=>'string'], +'sqlite_popen' => ['resource|false', 'filename'=>'string', 'mode='=>'int', 'error_message='=>'string'], 'sqlite_prev' => ['bool', 'result'=>''], 'sqlite_query' => ['SQLiteResult', 'dbhandle'=>'', 'query'=>'string', 'result_type='=>'int', 'error_msg='=>'string'], 'sqlite_rewind' => ['bool', 'result'=>''], @@ -11538,12 +11825,13 @@ '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'], 'srand' => ['void', 'seed='=>'int', 'mode='=>'int'], -'sscanf' => ['mixed', 'str'=>'string', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], +'sscanf' => ['int|null', 'str'=>'string', 'format'=>'string', '&w_war'=>'string|int|float|null', '&...w_vars='=>'string|int|float|null'], +'sscanf\'1' => ['array|null', 'str'=>'string', 'format'=>'string'], 'ssdeep_fuzzy_compare' => ['int', 'signature1'=>'string', 'signature2'=>'string'], 'ssdeep_fuzzy_hash' => ['string', 'to_hash'=>'string'], 'ssdeep_fuzzy_hash_filename' => ['string', 'file_name'=>'string'], @@ -11686,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' => ['array|false', 'str'=>'string', 'split_length='=>'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', 'brigade'=>'resource'], -'stream_bucket_new' => ['resource', 'stream'=>'resource', 'buffer'=>'string'], +'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'], @@ -11719,28 +12007,28 @@ '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}', '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'], 'stream_resolve_include_path' => ['string|false', 'filename'=>'string'], -'stream_select' => ['int|false', '&rw_read_streams'=>'resource[]', '&rw_write_streams'=>'resource[]|null', '&rw_except_streams'=>'resource[]|null', 'tv_sec'=>'?int', 'tv_usec='=>'?int'], +'stream_select' => ['int|false', '&rw_read_streams'=>'resource[]|null', '&rw_write_streams'=>'resource[]|null', '&rw_except_streams'=>'resource[]|null', 'tv_sec'=>'?int', 'tv_usec='=>'?int'], 'stream_set_blocking' => ['bool', 'socket'=>'resource', 'mode'=>'bool'], 'stream_set_chunk_size' => ['int|false', 'fp'=>'resource', 'chunk_size'=>'int'], 'stream_set_read_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], '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_get_name' => ['string', 'stream'=>'resource', 'want_peer'=>'bool'], +'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', 'stream'=>'resource', 'amount'=>'int', 'flags='=>'int', '&w_remote_addr='=>'string'], -'stream_socket_sendto' => ['int', 'stream'=>'resource', 'data'=>'string', 'flags='=>'int', 'target_addr='=>'string'], +'stream_socket_recvfrom' => ['string|false', 'stream'=>'resource', 'amount'=>'int', 'flags='=>'int', '&w_remote_addr='=>'string'], +'stream_socket_sendto' => ['int|false', 'stream'=>'resource', 'data'=>'string', 'flags='=>'int', 'target_addr='=>'string'], 'stream_socket_server' => ['resource|false', 'localaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'flags='=>'int', 'context='=>'resource'], 'stream_socket_shutdown' => ['bool', 'stream'=>'resource', 'how'=>'int'], 'stream_supports_lock' => ['bool', 'stream'=>'resource'], @@ -11772,37 +12060,37 @@ 'streamWrapper::stream_write' => ['int', 'data'=>'string'], 'streamWrapper::unlink' => ['bool', 'path'=>'string'], 'streamWrapper::url_stat' => ['array', 'path'=>'string', 'flags'=>'int'], -'strftime' => ['string', 'format'=>'string', 'timestamp='=>'int'], +'strftime' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'strip_tags' => ['string', 'str'=>'string', 'allowable_tags='=>'string'], 'stripcslashes' => ['string', 'str'=>'string'], -'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], +'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'stripslashes' => ['string', 'str'=>'string'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], -'strlen' => ['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'], +'strlen' => ['0|positive-int', 'string'=>'string'], +'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' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], +'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strptime' => ['array|false', 'datestr'=>'string', 'format'=>'string'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed'], 'strrev' => ['string', 'str'=>'string'], -'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], -'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], -'strspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'len='=>'int'], +'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' => ['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'], -'substr' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'int'], -'substr_compare' => ['int|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], -'substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], +'strval' => ['string', 'var'=>'__stringAndStringable|int|float|bool|resource|null'], +'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], +'substr_compare' => ['int<-1, 1>|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], +'substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], 'substr_replace' => ['string|array', 'str'=>'string|array', 'repl'=>'mixed', 'start'=>'mixed', 'length='=>'mixed'], 'suhosin_encrypt_cookie' => ['string', 'name'=>'string', 'value'=>'string'], 'suhosin_get_raw_cookies' => ['array'], @@ -11860,8 +12148,8 @@ 'svn_ls' => ['array', 'repos_url'=>'string', 'revision_no='=>'int', 'recurse='=>'bool', 'peg='=>'bool'], 'svn_mkdir' => ['bool', 'path'=>'string', 'log_message='=>'string'], 'svn_move' => ['mixed', 'src_path'=>'string', 'dst_path'=>'string', 'force='=>'bool|false'], -'svn_propget' => ['mixed', 'path'=>'string', 'property_name'=>'string', 'recurse='=>'bool|false', 'revision'=>'int'], -'svn_proplist' => ['mixed', 'path'=>'string', 'recurse='=>'bool|false', 'revision'=>'int'], +'svn_propget' => ['mixed', 'path'=>'string', 'property_name'=>'string', 'recurse='=>'bool|false', 'revision='=>'int'], +'svn_proplist' => ['mixed', 'path'=>'string', 'recurse='=>'bool|false', 'revision='=>'int'], 'svn_repos_create' => ['resource', 'path'=>'string', 'config='=>'array', 'fsconfig='=>'array'], 'svn_repos_fs' => ['resource', 'repos'=>'resource'], 'svn_repos_fs_begin_txn_for_commit' => ['resource', 'repos'=>'resource', 'rev'=>'int', 'author'=>'string', 'log_msg'=>'string'], @@ -12135,7 +12423,7 @@ 'sybase_fetch_assoc' => ['array', 'result'=>'resource'], 'sybase_fetch_field' => ['object', 'result'=>'resource', 'field_offset='=>'int'], 'sybase_fetch_object' => ['object', 'result'=>'resource', 'object='=>'mixed'], -'sybase_fetch_row' => ['array', 'result'=>'resource'], +'sybase_fetch_row' => ['array|false', 'result'=>'resource'], 'sybase_field_seek' => ['bool', 'result'=>'resource', 'field_offset'=>'int'], 'sybase_free_result' => ['bool', 'result'=>'resource'], 'sybase_get_last_message' => ['string'], @@ -12145,17 +12433,17 @@ 'sybase_min_server_severity' => ['void', 'severity'=>'int'], 'sybase_num_fields' => ['int', 'result'=>'resource'], 'sybase_num_rows' => ['int', 'result'=>'resource'], -'sybase_pconnect' => ['resource', 'servername='=>'string', 'username='=>'string', 'password='=>'string', 'charset='=>'string', 'appname='=>'string'], +'sybase_pconnect' => ['resource|false', 'servername='=>'string', 'username='=>'string', 'password='=>'string', 'charset='=>'string', 'appname='=>'string'], 'sybase_query' => ['mixed', 'query'=>'string', 'link_identifier='=>'resource'], 'sybase_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field'=>'mixed'], 'sybase_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'resource'], 'sybase_set_message_handler' => ['bool', 'handler'=>'callable', 'connection='=>'resource'], -'sybase_unbuffered_query' => ['resource', 'query'=>'string', 'link_identifier'=>'resource', 'store_result='=>'bool'], -'symbolObj::__construct' => ['void', 'map'=>'MapObj', 'symbolname'=>'string'], +'sybase_unbuffered_query' => ['resource|false', 'query'=>'string', 'link_identifier'=>'resource', 'store_result='=>'bool'], +'symbolObj::__construct' => ['void', 'map'=>'mapObj', 'symbolname'=>'string'], 'symbolObj::free' => ['void'], 'symbolObj::getPatternArray' => ['array'], 'symbolObj::getPointsArray' => ['array'], -'symbolObj::ms_newSymbolObj' => ['int', 'map'=>'MapObj', 'symbolname'=>'string'], +'symbolObj::ms_newSymbolObj' => ['int', 'map'=>'mapObj', 'symbolname'=>'string'], 'symbolObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'symbolObj::setImagePath' => ['int', 'filename'=>'string'], 'symbolObj::setPattern' => ['int', 'int'=>'array'], @@ -12182,18 +12470,18 @@ 'SyncSharedMemory::size' => ['bool'], 'SyncSharedMemory::write' => ['', 'string='=>'string', 'start='=>'int'], 'sys_get_temp_dir' => ['string'], -'sys_getloadavg' => ['array'], +'sys_getloadavg' => ['array|false'], 'syslog' => ['bool', 'priority'=>'int', 'message'=>'string'], -'system' => ['string', 'command'=>'string', '&w_return_value='=>'int'], +'system' => ['string|false', 'command'=>'string', '&w_return_value='=>'int'], 'taint' => ['bool', '&rw_string'=>'string', '&...w_other_strings='=>'string'], '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'], -'Thread::count' => ['int'], +'Thread::count' => ['0|positive-int'], 'Thread::getCreatorId' => ['int'], 'Thread::getCurrentThread' => ['Thread|null'], 'Thread::getCurrentThreadId' => ['int'], @@ -12218,7 +12506,7 @@ 'Thread::wait' => ['bool', 'timeout='=>'int'], 'Threaded::__construct' => ['void'], 'Threaded::chunk' => ['array', 'size'=>'int', 'preserve'=>'bool'], -'Threaded::count' => ['int'], +'Threaded::count' => ['0|positive-int'], 'Threaded::extend' => ['bool', 'class'=>'string'], 'Threaded::isRunning' => ['bool'], 'Threaded::isTerminated' => ['bool'], @@ -12240,7 +12528,7 @@ 'Throwable::getLine' => ['int'], 'Throwable::getMessage' => ['string'], 'Throwable::getPrevious' => ['Throwable|null'], -'Throwable::getTrace' => ['array'], +'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'], @@ -12257,10 +12545,10 @@ 'tidy::htmlver' => ['int'], 'tidy::isXhtml' => ['bool'], 'tidy::isXml' => ['bool'], -'tidy::parseFile' => ['bool', 'filename'=>'string', 'config='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], -'tidy::parseString' => ['bool', 'input'=>'string', 'config='=>'', 'encoding='=>'string'], -'tidy::repairFile' => ['string', 'filename'=>'string', 'config='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], -'tidy::repairString' => ['string', 'data'=>'string', 'config='=>'', 'encoding='=>'string'], +'tidy::parseFile' => ['bool', 'filename'=>'string', 'config='=>'mixed', 'encoding='=>'string', 'use_include_path='=>'bool'], +'tidy::parseString' => ['bool', 'input'=>'string', 'config='=>'mixed', 'encoding='=>'string'], +'tidy::repairFile' => ['string', 'filename'=>'string', 'config='=>'mixed', 'encoding='=>'string', 'use_include_path='=>'bool'], +'tidy::repairString' => ['string', 'data'=>'string', 'config='=>'mixed', 'encoding='=>'string'], 'tidy::root' => ['tidyNode'], 'tidy_access_count' => ['int', 'obj'=>'tidy'], 'tidy_clean_repair' => ['bool', 'obj'=>'tidy'], @@ -12269,23 +12557,23 @@ 'tidy_error_count' => ['int', 'obj'=>'tidy'], 'tidy_get_body' => ['tidyNode', 'obj'=>'tidy'], 'tidy_get_config' => ['array', 'obj'=>'tidy'], -'tidy_get_error_buffer' => ['string', 'obj'=>'tidy'], +'tidy_get_error_buffer' => ['string|false', 'obj'=>'tidy'], 'tidy_get_head' => ['tidyNode', 'obj'=>'tidy'], 'tidy_get_html' => ['tidyNode', 'obj'=>'tidy'], 'tidy_get_html_ver' => ['int', 'obj'=>'tidy'], -'tidy_get_opt_doc' => ['string', 'obj'=>'tidy', 'optname'=>'string'], +'tidy_get_opt_doc' => ['string|false', 'obj'=>'tidy', 'optname'=>'string'], 'tidy_get_output' => ['string', 'obj'=>'tidy'], 'tidy_get_release' => ['string'], 'tidy_get_root' => ['tidyNode', 'obj'=>'tidy'], 'tidy_get_status' => ['int', 'obj'=>'tidy'], -'tidy_getopt' => ['', 'option'=>'string', 'obj'=>'tidy'], +'tidy_getopt' => ['mixed', 'option'=>'string', 'obj'=>'tidy'], 'tidy_is_xhtml' => ['bool', 'obj'=>'tidy'], 'tidy_is_xml' => ['bool', 'obj'=>'tidy'], 'tidy_load_config' => ['void', 'filename'=>'string', 'encoding'=>'string'], -'tidy_parse_file' => ['tidy', 'file'=>'string', 'config_options='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], -'tidy_parse_string' => ['tidy', 'input'=>'string', 'config_options='=>'', 'encoding='=>'string'], -'tidy_repair_file' => ['string', 'filename'=>'string', 'config_file='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], -'tidy_repair_string' => ['string', 'data'=>'string', 'config_file='=>'', 'encoding='=>'string'], +'tidy_parse_file' => ['tidy|false', 'file'=>'string', 'config_options='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], +'tidy_parse_string' => ['tidy|false', 'input'=>'string', 'config_options='=>'', 'encoding='=>'string'], +'tidy_repair_file' => ['string|false', 'filename'=>'string', 'config_file='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], +'tidy_repair_string' => ['string|false', 'data'=>'string', 'config_file='=>'', 'encoding='=>'string'], 'tidy_reset_config' => ['bool'], 'tidy_save_config' => ['bool', 'filename'=>'string'], 'tidy_set_encoding' => ['bool', 'encoding'=>'string'], @@ -12301,21 +12589,21 @@ 'tidyNode::isJste' => ['bool'], 'tidyNode::isPhp' => ['bool'], 'tidyNode::isText' => ['bool'], -'time' => ['int'], -'time_nanosleep' => ['array{0:int,1:int}|bool', 'seconds'=>'int', 'nanoseconds'=>'int'], +'time' => ['positive-int'], +'time_nanosleep' => ['array{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_location_get' => ['array|false', 'object'=>'DateTimeZone'], +'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', 'timezone'=>'string'], -'timezone_transitions_get' => ['array|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], +'timezone_open' => ['DateTimeZone|false', 'timezone'=>'string'], +'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'], @@ -12347,7 +12635,7 @@ 'TokyoTyrantIterator::valid' => ['bool'], 'TokyoTyrantQuery::__construct' => ['void', 'table'=>'TokyoTyrantTable'], 'TokyoTyrantQuery::addCond' => ['mixed', 'name'=>'string', 'op'=>'int', 'expr'=>'string'], -'TokyoTyrantQuery::count' => ['int'], +'TokyoTyrantQuery::count' => ['0|positive-int'], 'TokyoTyrantQuery::current' => ['array'], 'TokyoTyrantQuery::hint' => ['string'], 'TokyoTyrantQuery::key' => ['string'], @@ -12372,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_get_error_code' => ['int', 'obj'=>'Transliterator'], -'transliterator_get_error_message' => ['string', 'obj'=>'Transliterator'], -'transliterator_list_ids' => ['array'], +'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' => ['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'], @@ -12560,9 +12848,9 @@ 'TypeError::getLine' => ['int'], 'TypeError::getMessage' => ['string'], 'TypeError::getPrevious' => ['Throwable|TypeError|null'], -'TypeError::getTrace' => ['array'], +'TypeError::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'TypeError::getTraceAsString' => ['string'], -'uasort' => ['bool', '&rw_array_arg'=>'array', 'cmp_function'=>'callable(mixed,mixed):int'], +'uasort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'ucfirst' => ['string', 'str'=>'string'], 'UConverter::__construct' => ['void', 'destination_encoding'=>'string', 'source_encoding='=>'string'], 'UConverter::convert' => ['string', 'str'=>'string', 'reverse='=>'bool'], @@ -12611,7 +12899,7 @@ 'ui\draw\text\font\fontfamilies' => ['array'], 'ui\quit' => ['void'], 'ui\run' => ['void', 'flags='=>'int'], -'uksort' => ['bool', '&rw_array_arg'=>'array', 'cmp_function'=>'callable(mixed,mixed):int'], +'uksort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(array-key,array-key):int'], 'umask' => ['int', 'mask='=>'int'], 'UnderflowException::__clone' => ['void'], 'UnderflowException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnderflowException)'], @@ -12621,7 +12909,7 @@ 'UnderflowException::getLine' => ['int'], 'UnderflowException::getMessage' => ['string'], 'UnderflowException::getPrevious' => ['Throwable|UnderflowException|null'], -'UnderflowException::getTrace' => ['array'], +'UnderflowException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'UnderflowException::getTraceAsString' => ['string'], 'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnexpectedValueException)'], @@ -12631,32 +12919,41 @@ 'UnexpectedValueException::getLine' => ['int'], 'UnexpectedValueException::getMessage' => ['string'], 'UnexpectedValueException::getPrevious' => ['Throwable|UnexpectedValueException|null'], -'UnexpectedValueException::getTrace' => ['array'], +'UnexpectedValueException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'UnexpectedValueException::getTraceAsString' => ['string'], -'uniqid' => ['string', 'prefix='=>'string', 'more_entropy='=>'bool'], -'unixtojd' => ['int', 'timestamp='=>'int'], +'uniqid' => ['non-empty-string', 'prefix='=>'string', 'more_entropy='=>'bool'], +'unixtojd' => ['int|false', 'timestamp='=>'int'], 'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'], -'unpack' => ['array', 'format'=>'string', 'data'=>'string', 'offset='=>'int'], -'unregister_tick_function' => ['void', 'function_name'=>'string'], +'unpack' => ['array|false', 'format'=>'string', 'data'=>'string', 'offset='=>'int'], +'unregister_tick_function' => ['void', 'function_name'=>'callable'], 'unserialize' => ['mixed', 'variable_representation'=>'string', 'allowed_classes='=>'array{allowed_classes?:string[]|bool}'], -'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'], 'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'], +'uopz_add_function' => ['bool', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool', '$all'=>'bool'], +'uopz_add_function\'1' => ['bool', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool'], 'uopz_allow_exit' => ['void', 'allow'=>'bool'], 'uopz_backup' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_backup\'1' => ['void', 'function'=>'string'], 'uopz_compose' => ['void', 'name'=>'string', 'classes'=>'array', 'methods='=>'array', 'properties='=>'array', 'flags='=>'int'], '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_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_mock' => ['mixed', 'class'=>'string'], -'uopz_get_return' => ['mixed', 'class='=>'string', 'function'=>'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_implement' => ['void', 'class'=>'string', 'interface'=>'string'], 'uopz_overload' => ['void', 'opcode'=>'int', 'callable'=>'Callable'], 'uopz_redefine' => ['void', 'class'=>'string', 'constant'=>'string', 'value'=>'mixed'], @@ -12666,18 +12963,22 @@ 'uopz_restore' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_restore\'1' => ['void', 'function'=>'string'], 'uopz_set_mock' => ['void', 'class'=>'string', 'mock'=>'object|string'], -'uopz_set_return' => ['bool', 'class='=>'string', 'function'=>'string', 'value'=>'mixed', 'execute='=>'bool'], +'uopz_set_property' => ['void', 'class'=>'string', '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_unset_mock' => ['void', 'class'=>'string'], -'uopz_unset_return' => ['bool', 'class='=>'string', 'function'=>'string'], +'uopz_unset_return' => ['bool', 'class='=>'string', 'function='=>'string'], 'uopz_unset_return\'1' => ['bool', 'function'=>'string'], 'urldecode' => ['string', 'str'=>'string'], 'urlencode' => ['string', 'str'=>'string'], 'use_soap_error_handler' => ['bool', 'handler='=>'bool'], 'usleep' => ['void', 'micro_seconds'=>'int'], -'usort' => ['bool', '&rw_array_arg'=>'array', 'cmp_function'=>'callable(mixed,mixed):int'], +'usort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'utf8_decode' => ['string', 'data'=>'string'], 'utf8_encode' => ['string', 'data'=>'string'], 'V8Js::__construct' => ['void', 'object_name='=>'string', 'variables='=>'array', 'extensions='=>'array', 'report_uncaught_exceptions='=>'bool', 'snapshot_blob='=>'string'], @@ -12713,7 +13014,7 @@ 'V8JsScriptException::getLine' => ['int'], 'V8JsScriptException::getMessage' => ['string'], 'V8JsScriptException::getPrevious' => ['Exception|Throwable'], -'V8JsScriptException::getTrace' => ['array'], +'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'], @@ -12769,12 +13070,12 @@ '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'], -'Volatile::count' => ['int'], +'Volatile::count' => ['0|positive-int'], 'Volatile::extend' => ['bool', 'class'=>'string'], 'Volatile::isRunning' => ['bool'], 'Volatile::isTerminated' => ['bool'], @@ -12807,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='=>''], @@ -12821,7 +13122,7 @@ 'wddx_serialize_value' => ['string', 'var'=>'mixed', 'comment='=>'string'], 'wddx_serialize_vars' => ['string', 'var_name'=>'mixed', '...vars='=>'mixed'], 'WeakMap::__construct' => ['void'], -'WeakMap::count' => ['int'], +'WeakMap::count' => ['0|positive-int'], 'WeakMap::current' => ['mixed'], 'WeakMap::key' => ['object'], 'WeakMap::next' => ['void'], @@ -12860,18 +13161,18 @@ 'wincache_refresh_if_changed' => ['bool', 'files='=>'array'], 'wincache_rplist_fileinfo' => ['array', 'summaryonly='=>'bool'], 'wincache_rplist_meminfo' => ['array'], -'wincache_scache_info' => ['array', 'summaryonly='=>'bool'], -'wincache_scache_meminfo' => ['array'], +'wincache_scache_info' => ['array|false', 'summaryonly='=>'bool'], +'wincache_scache_meminfo' => ['array|false'], 'wincache_ucache_add' => ['bool', 'key'=>'string', 'value'=>'', 'ttl='=>'int'], 'wincache_ucache_add\'1' => ['bool', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'wincache_ucache_cas' => ['bool', 'key'=>'string', 'old_value'=>'int', 'new_value'=>'int'], 'wincache_ucache_clear' => ['bool'], -'wincache_ucache_dec' => ['mixed', 'key'=>'string', 'dec_by='=>'int', 'success='=>'bool'], +'wincache_ucache_dec' => ['mixed', 'key'=>'string', 'dec_by='=>'int', '&w_success='=>'bool'], 'wincache_ucache_delete' => ['bool', 'key'=>'mixed'], 'wincache_ucache_exists' => ['bool', 'key'=>'string'], 'wincache_ucache_get' => ['mixed', 'key'=>'mixed', '&w_success='=>'bool'], -'wincache_ucache_inc' => ['mixed', 'key'=>'string', 'inc_by='=>'int', 'success='=>'bool'], -'wincache_ucache_info' => ['array', 'summaryonly='=>'bool', 'key='=>'string'], +'wincache_ucache_inc' => ['int|false', 'key'=>'string', 'inc_by='=>'int', '&w_success='=>'bool'], +'wincache_ucache_info' => ['array|false', 'summaryonly='=>'bool', 'key='=>'string'], 'wincache_ucache_meminfo' => ['array'], 'wincache_ucache_set' => ['bool', 'key'=>'', 'value'=>'', 'ttl='=>'int'], 'wincache_ucache_set\'1' => ['bool', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], @@ -12880,7 +13181,7 @@ 'Worker::__construct' => ['void'], 'Worker::chunk' => ['array', 'size'=>'int', 'preserve'=>'bool'], 'Worker::collect' => ['int', 'collector='=>'Callable'], -'Worker::count' => ['int'], +'Worker::count' => ['0|positive-int'], 'Worker::getCreatorId' => ['int'], 'Worker::getCurrentThread' => ['Thread'], 'Worker::getCurrentThreadId' => ['int'], @@ -12950,12 +13251,13 @@ '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'], 'xdebug_debug_zval' => ['void', '...varName'=>'string'], 'xdebug_debug_zval_stdout' => ['void', '...varName'=>'string'], 'xdebug_disable' => ['void'], @@ -12967,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'], @@ -12976,9 +13278,10 @@ 'xdebug_is_debugger_active' => ['bool'], 'xdebug_is_enabled' => ['bool'], 'xdebug_memory_usage' => ['int'], +'xdebug_notify' => ['bool', 'data'=>'mixed'], 'xdebug_peak_memory_usage' => ['int'], -'xdebug_print_function_stack' => ['array', 'message='=>'string', 'options=' => 'int'], -'xdebug_set_filter' => ['void', 'group' => 'int', 'list_type' => 'int', 'configuration' => 'array'], +'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[]'], @@ -13009,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'], @@ -13043,7 +13346,7 @@ 'XMLDiff\Memory::diff' => ['string', 'from'=>'string', 'to'=>'string'], 'XMLDiff\Memory::merge' => ['string', 'src'=>'string', 'diff'=>'string'], 'XMLReader::close' => ['bool'], -'XMLReader::expand' => ['DOMNode', 'basenode='=>'DOMNode'], +'XMLReader::expand' => ['DOMNode|false', 'basenode='=>'DOMNode'], 'XMLReader::getAttribute' => ['string|null', 'name'=>'string'], 'XMLReader::getAttributeNo' => ['string|null', 'index'=>'int'], 'XMLReader::getAttributeNs' => ['string|null', 'name'=>'string', 'namespaceuri'=>'string'], @@ -13057,7 +13360,7 @@ 'XMLReader::moveToFirstAttribute' => ['bool'], 'XMLReader::moveToNextAttribute' => ['bool'], 'XMLReader::next' => ['bool', 'localname='=>'string'], -'XMLReader::open' => ['bool', 'uri'=>'string', 'encoding='=>'?string', 'options='=>'int'], +'XMLReader::open' => ['bool|XMLReader', 'uri'=>'string', 'encoding='=>'?string', 'options='=>'int'], 'XMLReader::read' => ['bool'], 'XMLReader::readInnerXML' => ['string'], 'XMLReader::readOuterXML' => ['string'], @@ -13066,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'], @@ -13099,7 +13402,7 @@ 'XMLWriter::setIndent' => ['bool', 'indent'=>'bool'], 'XMLWriter::setIndentString' => ['bool', 'indentstring'=>'string'], 'XMLWriter::startAttribute' => ['bool', 'name'=>'string'], -'XMLWriter::startAttributeNS' => ['bool', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], +'XMLWriter::startAttributeNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string'], 'XMLWriter::startCData' => ['bool'], 'XMLWriter::startComment' => ['bool'], 'XMLWriter::startDocument' => ['bool', 'version='=>'string', 'encoding='=>'string', 'standalone='=>'string'], @@ -13108,11 +13411,11 @@ 'XMLWriter::startDTDElement' => ['bool', 'qualifiedname'=>'string'], 'XMLWriter::startDTDEntity' => ['bool', 'name'=>'string', 'isparam'=>'bool'], 'XMLWriter::startElement' => ['bool', 'name'=>'string'], -'XMLWriter::startElementNS' => ['bool', 'prefix'=>'string', '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'], -'XMLWriter::writeAttributeNS' => ['bool', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], +'XMLWriter::writeAttributeNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], 'XMLWriter::writeCData' => ['bool', 'content'=>'string'], 'XMLWriter::writeComment' => ['bool', 'content'=>'string'], 'XMLWriter::writeDTD' => ['bool', 'name'=>'string', 'publicid='=>'string', 'systemid='=>'string', 'subset='=>'string'], @@ -13120,7 +13423,7 @@ 'XMLWriter::writeDTDElement' => ['bool', 'name'=>'string', 'content'=>'string'], 'XMLWriter::writeDTDEntity' => ['bool', 'name'=>'string', 'content'=>'string', 'pe'=>'bool', 'pubid'=>'string', 'sysid'=>'string', 'ndataid'=>'string'], 'XMLWriter::writeElement' => ['bool', 'name'=>'string', 'content='=>'string|null'], -'XMLWriter::writeElementNS' => ['bool', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content='=>'string|null'], +'XMLWriter::writeElementNS' => ['bool', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string', 'content='=>'string|null'], 'XMLWriter::writePI' => ['bool', 'target'=>'string', 'content'=>'string'], 'XMLWriter::writeRaw' => ['bool', 'content'=>'string'], 'xmlwriter_end_attribute' => ['bool', 'xmlwriter'=>'resource'], @@ -13135,13 +13438,13 @@ 'xmlwriter_end_pi' => ['bool', 'xmlwriter'=>'resource'], 'xmlwriter_flush' => ['', 'xmlwriter'=>'resource', 'empty='=>'bool'], 'xmlwriter_full_end_element' => ['bool', 'xmlwriter'=>'resource'], -'xmlwriter_open_memory' => ['resource'], -'xmlwriter_open_uri' => ['resource', 'source'=>'string'], +'xmlwriter_open_memory' => ['resource|false'], +'xmlwriter_open_uri' => ['resource|false', 'source'=>'string'], 'xmlwriter_output_memory' => ['string', 'xmlwriter'=>'resource', 'flush='=>'bool'], 'xmlwriter_set_indent' => ['bool', 'xmlwriter'=>'resource', 'indent'=>'bool'], 'xmlwriter_set_indent_string' => ['bool', 'xmlwriter'=>'resource', 'indentstring'=>'string'], 'xmlwriter_start_attribute' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], -'xmlwriter_start_attribute_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], +'xmlwriter_start_attribute_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string'], 'xmlwriter_start_cdata' => ['bool', 'xmlwriter'=>'resource'], 'xmlwriter_start_comment' => ['bool', 'xmlwriter'=>'resource'], 'xmlwriter_start_document' => ['bool', 'xmlwriter'=>'resource', 'version'=>'string', 'encoding'=>'string', 'standalone'=>'string'], @@ -13150,11 +13453,11 @@ '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|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'], -'xmlwriter_write_attribute_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], +'xmlwriter_write_attribute_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], 'xmlwriter_write_cdata' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xmlwriter_write_comment' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xmlwriter_write_dtd' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'pubid'=>'string', 'sysid'=>'string', 'subset'=>'string'], @@ -13162,7 +13465,7 @@ 'xmlwriter_write_dtd_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], 'xmlwriter_write_dtd_entity' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string', 'pe'=>'int', 'pubid'=>'string', 'sysid'=>'string', 'ndataid'=>'string'], 'xmlwriter_write_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], -'xmlwriter_write_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], +'xmlwriter_write_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string|null', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], 'xmlwriter_write_pi' => ['bool', 'xmlwriter'=>'resource', 'target'=>'string', 'content'=>'string'], 'xmlwriter_write_raw' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], 'xpath_new_context' => ['XPathContext', 'dom_document'=>'DOMDocument'], @@ -13198,7 +13501,7 @@ 'xslt_set_scheme_handler' => ['', 'xh'=>'', 'handlers'=>'array'], 'xslt_set_scheme_handlers' => ['', 'xh'=>'', 'handlers'=>'array'], 'xslt_setopt' => ['', 'processor'=>'', 'newmask'=>'int'], -'XSLTProcessor::getParameter' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], +'XSLTProcessor::getParameter' => ['string|false', 'namespaceuri'=>'string', 'localname'=>'string'], 'XsltProcessor::getSecurityPrefs' => ['int'], 'XSLTProcessor::hasExsltSupport' => ['bool'], 'XSLTProcessor::importStylesheet' => ['bool', 'stylesheet'=>'object'], @@ -13208,9 +13511,9 @@ 'XSLTProcessor::setParameter\'1' => ['bool', 'namespace'=>'string', 'options'=>'array'], 'XSLTProcessor::setProfiling' => ['bool', 'filename'=>'string'], 'XsltProcessor::setSecurityPrefs' => ['int', 'securityPrefs'=>'int'], -'XSLTProcessor::transformToDoc' => ['DOMDocument', 'doc'=>'DOMNode'], +'XSLTProcessor::transformToDoc' => ['DOMDocument|false', 'doc'=>'DOMNode'], 'XSLTProcessor::transformToURI' => ['int', 'doc'=>'DOMDocument', 'uri'=>'string'], -'XSLTProcessor::transformToXML' => ['string|false', 'doc'=>'DOMDocument'], +'XSLTProcessor::transformToXML' => ['string|false|null', 'doc'=>'DOMDocument|SimpleXMLElement'], 'Yaconf::get' => ['mixed', 'name'=>'string', 'default_value='=>'mixed'], 'Yaconf::has' => ['bool', 'name'=>'string'], 'Yaf_Action_Abstract::__construct' => ['void', 'request'=>'Yaf_Request_Abstract', 'response'=>'Yaf_Response_Abstract', 'view'=>'Yaf_View_Interface', 'invokeArgs='=>'?array'], @@ -13233,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'], @@ -13248,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_Ini::count' => ['void'], -'Yaf_Config_Ini::current' => ['void'], -'Yaf_Config_Ini::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Ini::key' => ['void'], +'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' => ['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::count' => ['void'], -'Yaf_Config_Simple::current' => ['void'], -'Yaf_Config_Simple::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Simple::key' => ['void'], +'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' => ['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'], @@ -13358,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'], @@ -13544,10 +13881,10 @@ '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' => ['void'], +'Yaf_Session::count' => ['0|positive-int'], 'Yaf_Session::current' => ['void'], 'Yaf_Session::del' => ['void', 'name'=>'string'], 'Yaf_Session::get' => ['mixed', 'name'=>'string'], @@ -13563,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'], @@ -13713,21 +14051,21 @@ 'ZendAPI_Queue::zendapi_queue' => ['ZendAPI_Queue', 'queue_url'=>'string'], 'zip_close' => ['void', 'zip'=>'resource'], 'zip_entry_close' => ['bool', 'zip_ent'=>'resource'], -'zip_entry_compressedsize' => ['int', 'zip_entry'=>'resource'], -'zip_entry_compressionmethod' => ['string', 'zip_entry'=>'resource'], -'zip_entry_filesize' => ['int', 'zip_entry'=>'resource'], -'zip_entry_name' => ['string', 'zip_entry'=>'resource'], +'zip_entry_compressedsize' => ['int|false', 'zip_entry'=>'resource'], +'zip_entry_compressionmethod' => ['string|false', 'zip_entry'=>'resource'], +'zip_entry_filesize' => ['int|false', 'zip_entry'=>'resource'], +'zip_entry_name' => ['string|false', 'zip_entry'=>'resource'], 'zip_entry_open' => ['bool', 'zip_dp'=>'resource', 'zip_entry'=>'resource', 'mode='=>'string'], 'zip_entry_read' => ['string|false', 'zip_entry'=>'resource', 'len='=>'int'], -'zip_open' => ['resource', 'filename'=>'string'], -'zip_read' => ['resource', 'zip'=>'resource'], +'zip_open' => ['resource|false|int', 'filename'=>'string'], +'zip_read' => ['resource|false|int', 'zip'=>'resource'], 'ZipArchive::addEmptyDir' => ['bool', 'dirname'=>'string'], 'ZipArchive::addFile' => ['bool', 'filepath'=>'string', 'entryname='=>'string', 'start='=>'int', 'length='=>'int'], 'ZipArchive::addFromString' => ['bool', 'entryname'=>'string', 'content'=>'string'], 'ZipArchive::addGlob' => ['bool', 'pattern'=>'string', 'flags='=>'int', 'options='=>'array'], 'ZipArchive::addPattern' => ['bool', 'pattern'=>'string', 'path='=>'string', 'options='=>'array'], 'ZipArchive::close' => ['bool'], -'ZipArchive::count' => ['int'], +'ZipArchive::count' => ['0|positive-int'], 'ZipArchive::createEmptyDir' => ['bool', 'dirname'=>'string'], 'ZipArchive::deleteIndex' => ['bool', 'index'=>'int'], 'ZipArchive::deleteName' => ['bool', 'name'=>'string'], @@ -13743,7 +14081,7 @@ 'ZipArchive::getStatusString' => ['string'], 'ZipArchive::getStream' => ['resource|false', 'entryname'=>'string'], 'ZipArchive::locateName' => ['int|false', 'filename'=>'string', 'flags='=>'int'], -'ZipArchive::open' => ['mixed', 'source'=>'string', 'flags='=>'int'], +'ZipArchive::open' => ['ZipArchive::ER_*|true', 'source'=>'string', 'flags='=>'int'], 'ZipArchive::renameIndex' => ['bool', 'index'=>'int', 'new_name'=>'string'], 'ZipArchive::renameName' => ['bool', 'name'=>'string', 'new_name'=>'string'], 'ZipArchive::setArchiveComment' => ['bool', 'comment'=>'string'], @@ -13763,7 +14101,7 @@ 'ZipArchive::unchangeIndex' => ['bool', 'index'=>'int'], 'ZipArchive::unchangeName' => ['bool', 'name'=>'string'], 'zlib_decode' => ['string|false', 'data'=>'string', 'max_decoded_len='=>'int'], -'zlib_encode' => ['string', 'data'=>'string', 'encoding'=>'int', 'level='=>'string|int'], +'zlib_encode' => ['string|false', 'data'=>'string', 'encoding'=>'int', 'level='=>'string|int'], 'zlib_get_coding_type' => ['string|false'], 'ZMQ::__construct' => ['void'], 'ZMQContext::__construct' => ['void', 'io_threads='=>'int', 'is_persistent='=>'bool'], @@ -13781,7 +14119,7 @@ 'ZMQDevice::setTimerTimeout' => ['ZMQDevice', 'timeout'=>'int'], 'ZMQPoll::add' => ['string', 'entry'=>'mixed', 'type'=>'int'], 'ZMQPoll::clear' => ['ZMQPoll'], -'ZMQPoll::count' => ['int'], +'ZMQPoll::count' => ['0|positive-int'], 'ZMQPoll::getLastErrors' => ['array'], 'ZMQPoll::poll' => ['int', '&w_readable'=>'array', '&w_writable'=>'array', 'timeout='=>'int'], 'ZMQPoll::remove' => ['bool', 'item'=>'mixed'], 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 new file mode 100644 index 0000000000..bb05fe2b6a --- /dev/null +++ b/resources/functionMap_php74delta.php @@ -0,0 +1,63 @@ + [ + '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::cdef' => ['FFI', 'code='=>'string', 'lib='=>'?string'], + '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::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::type' => ['FFI\CType', 'type'=>'string'], + 'fread' => ['string|false', 'fp'=>'resource', 'length'=>'positive-int'], + 'get_mangled_object_vars' => ['array', 'obj'=>'object'], + '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'], + 'sapi_windows_set_ctrl_handler' => ['bool', 'callable'=>'callable(int):void', 'add='=>'bool'], + 'ReflectionProperty::getType' => ['?ReflectionType'], + 'ReflectionProperty::hasType' => ['bool'], + 'ReflectionProperty::isInitialized' => ['bool', 'object='=>'?object'], + 'ReflectionReference::fromArrayElement' => ['?ReflectionReference', 'array'=>'array', 'key'=>'int|string'], + 'ReflectionReference::getId' => ['string'], + 'SQLite3Stmt::getSQL' => ['string', 'expanded='=>'bool'], + 'strip_tags' => ['string', 'str'=>'string', 'allowable_tags='=>'string|array'], + 'WeakReference::create' => ['WeakReference', 'referent'=>'object'], + 'WeakReference::get' => ['?object'], + 'proc_open' => ['resource|false', 'command'=>'string|list', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'], + ], + 'old' => [ + 'implode\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], + ], +]; diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php new file mode 100644 index 0000000000..bca5fef36b --- /dev/null +++ b/resources/functionMap_php80delta.php @@ -0,0 +1,310 @@ + [ + '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'], + 'date_format' => ['string', 'object'=>'DateTimeInterface', 'format'=>'string'], + '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{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'], + '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-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' => ['__benevolent', 'width'=>'int<1, max>', 'height'=>'int<1, max>'], + 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], + 'imagecreatefromgd' => ['false|object', 'filename'=>'string'], + 'imagecreatefromgd2' => ['false|object', 'filename'=>'string'], + 'imagecreatefromgd2part' => ['false|object', 'filename'=>'string', 'srcx'=>'int', 'srcy'=>'int', 'width'=>'int', 'height'=>'int'], + 'imagecreatefromgif' => ['false|object', 'filename'=>'string'], + 'imagecreatefromjpeg' => ['false|object', 'filename'=>'string'], + 'imagecreatefrompng' => ['false|object', 'filename'=>'string'], + 'imagecreatefromstring' => ['false|object', 'image'=>'string'], + 'imagecreatefromwbmp' => ['false|object', 'filename'=>'string'], + 'imagecreatefromwebp' => ['false|object', 'filename'=>'string'], + 'imagecreatefromxbm' => ['false|object', 'filename'=>'string'], + 'imagecreatefromxpm' => ['false|object', 'filename'=>'string'], + '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'], + 'imagegrabscreen' => ['false|object'], + 'imagegrabwindow' => ['false|object', 'window_handle'=>'int', 'client_area='=>'int'], + 'imagejpeg' => ['bool', 'im'=>'GdImage', 'filename='=>'string|resource|null', 'quality='=>'int'], + 'imagerotate' => ['false|object', 'src_im'=>'resource', 'angle'=>'float', 'bgdcolor'=>'int', 'ignoretransparent='=>'int'], + 'imagescale' => ['false|object', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], + 'ldap_set_rebind_proc' => ['bool', 'ldap'=>'resource', 'callback'=>'?callable'], + 'mb_decode_numericentity' => ['string|false', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string'], + 'mb_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' => ['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' => ['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', '&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-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' => ['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'], + 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], + 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], + 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], + 'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'], + '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'], + 'xml_parser_get_option' => ['mixed|false', 'parser'=>'XMLParser', 'option'=>'int'], + 'xml_parser_set_option' => ['bool', 'parser'=>'XMLParser', 'option'=>'int', 'value'=>'mixed'], + 'xmlwriter_end_attribute' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_cdata' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_comment' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_document' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_dtd' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_dtd_attlist' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_dtd_element' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_dtd_entity' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_element' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_end_pi' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_flush' => ['mixed', 'xmlwriter'=>'XMLWriter', 'empty='=>'bool'], + 'xmlwriter_full_end_element' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_open_memory' => ['XMLWriter'], + 'xmlwriter_open_uri' => ['XMLWriter', 'source'=>'string'], + 'xmlwriter_output_memory' => ['string', 'xmlwriter'=>'XMLWriter', 'flush='=>'bool'], + 'xmlwriter_set_indent' => ['bool', 'xmlwriter'=>'XMLWriter', 'indent'=>'bool'], + 'xmlwriter_set_indent_string' => ['bool', 'xmlwriter'=>'XMLWriter', 'indentstring'=>'string'], + 'xmlwriter_start_attribute' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string'], + 'xmlwriter_start_attribute_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], + 'xmlwriter_start_cdata' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_start_comment' => ['bool', 'xmlwriter'=>'XMLWriter'], + 'xmlwriter_start_document' => ['bool', 'xmlwriter'=>'XMLWriter', 'version='=>'string', 'encoding='=>'string', 'standalone='=>'string'], + 'xmlwriter_start_dtd' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'publicid='=>'string', 'sysid='=>'string'], + 'xmlwriter_start_dtd_attlist' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string'], + '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|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'], + 'xmlwriter_write_attribute_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], + 'xmlwriter_write_cdata' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], + 'xmlwriter_write_comment' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], + 'xmlwriter_write_dtd' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'publicid='=>'string', 'sysid='=>'string', 'subset='=>'string'], + 'xmlwriter_write_dtd_attlist' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'content'=>'string'], + 'xmlwriter_write_dtd_element' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'content'=>'string'], + 'xmlwriter_write_dtd_entity' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'content'=>'string', 'pe'=>'bool', 'publicid'=>'string', 'sysid'=>'string', 'ndataid'=>'string'], + 'xmlwriter_write_element' => ['bool', 'xmlwriter'=>'XMLWriter', 'name'=>'string', 'content'=>'string'], + 'xmlwriter_write_element_ns' => ['bool', 'xmlwriter'=>'XMLWriter', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], + 'xmlwriter_write_pi' => ['bool', 'xmlwriter'=>'XMLWriter', 'target'=>'string', 'content'=>'string'], + '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'], + 'date_format' => ['string|false', 'object'=>'DateTimeInterface', 'format'=>'string'], + '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' => ['__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-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'], + 'imagecreatefrombmp' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromgd' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromgd2' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromgd2part' => ['resource|false', 'filename'=>'string', 'srcx'=>'int', 'srcy'=>'int', 'width'=>'int', 'height'=>'int'], + 'imagecreatefromgif' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromjpeg' => ['resource|false', 'filename'=>'string'], + 'imagecreatefrompng' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromstring' => ['resource|false', 'image'=>'string'], + 'imagecreatefromwbmp' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromwebp' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromxbm' => ['resource|false', 'filename'=>'string'], + 'imagecreatefromxpm' => ['resource|false', 'filename'=>'string'], + 'imagecreatetruecolor' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecrop' => ['resource|false', 'im'=>'resource', 'rect'=>'array'], + 'imagecropauto' => ['resource|false', 'im'=>'resource', 'mode'=>'int', 'threshold'=>'float', 'color'=>'int'], + 'imagegetclip' => ['array|false', 'im'=>'resource'], + 'imagegrabscreen' => ['false|resource'], + 'imagegrabwindow' => ['false|resource', 'window_handle'=>'int', 'client_area='=>'int'], + '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'], + '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' => ['__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'], + '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' => ['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'], + 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], + 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], + 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], + 'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], + 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string|null'], + 'xml_parser_create' => ['resource', 'encoding='=>'string'], + 'xml_parser_create_ns' => ['resource', 'encoding='=>'string', 'sep='=>'string'], + 'xml_parser_free' => ['bool', 'parser'=>'resource'], + 'xml_parser_get_option' => ['mixed|false', 'parser'=>'resource', 'option'=>'int'], + 'xml_parser_set_option' => ['bool', 'parser'=>'resource', 'option'=>'int', 'value'=>'mixed'], + 'xmlwriter_end_attribute' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_cdata' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_comment' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_document' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_dtd' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_dtd_attlist' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_dtd_element' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_dtd_entity' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_element' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_end_pi' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_flush' => ['mixed', 'xmlwriter'=>'resource', 'empty='=>'bool'], + 'xmlwriter_full_end_element' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_open_memory' => ['resource'], + 'xmlwriter_open_uri' => ['resource', 'source'=>'string'], + 'xmlwriter_output_memory' => ['string', 'xmlwriter'=>'resource', 'flush='=>'bool'], + 'xmlwriter_set_indent' => ['bool', 'xmlwriter'=>'resource', 'indent'=>'bool'], + 'xmlwriter_set_indent_string' => ['bool', 'xmlwriter'=>'resource', 'indentstring'=>'string'], + 'xmlwriter_start_attribute' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], + 'xmlwriter_start_attribute_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string'], + 'xmlwriter_start_cdata' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_start_comment' => ['bool', 'xmlwriter'=>'resource'], + 'xmlwriter_start_document' => ['bool', 'xmlwriter'=>'resource', 'version='=>'string', 'encoding='=>'string', 'standalone='=>'string'], + 'xmlwriter_start_dtd' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'publicid='=>'string', 'sysid='=>'string'], + 'xmlwriter_start_dtd_attlist' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string'], + '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|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'], + 'xmlwriter_write_attribute_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], + 'xmlwriter_write_cdata' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], + 'xmlwriter_write_comment' => ['bool', 'xmlwriter'=>'resource', 'content'=>'string'], + 'xmlwriter_write_dtd' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'publicid='=>'string', 'sysid='=>'string', 'subset='=>'string'], + 'xmlwriter_write_dtd_attlist' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], + 'xmlwriter_write_dtd_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], + 'xmlwriter_write_dtd_entity' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string', 'pe'=>'bool', 'publicid'=>'string', 'sysid'=>'string', 'ndataid'=>'string'], + 'xmlwriter_write_element' => ['bool', 'xmlwriter'=>'resource', 'name'=>'string', 'content'=>'string'], + 'xmlwriter_write_element_ns' => ['bool', 'xmlwriter'=>'resource', 'prefix'=>'string', 'name'=>'string', 'uri'=>'string', 'content'=>'string'], + 'xmlwriter_write_pi' => ['bool', 'xmlwriter'=>'resource', 'target'=>'string', 'content'=>'string'], + 'xmlwriter_write_raw' => ['bool', 'xmlwriter'=>'resource', '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 new file mode 100644 index 0000000000..6136b1c068 --- /dev/null +++ b/resources/functionMetadata.php @@ -0,0 +1,1605 @@ + 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], + 'Cassandra\\Exception\\AlreadyExistsException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\AuthenticationException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\ConfigurationException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\DivideByZeroException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\DomainException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\ExecutionException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\InvalidArgumentException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\InvalidQueryException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\InvalidSyntaxException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\IsBootstrappingException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\LogicException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\OverloadedException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\ProtocolException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\RangeException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\ReadTimeoutException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\RuntimeException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\ServerException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\TimeoutException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\TruncateException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\UnauthorizedException::__construct' => ['hasSideEffects' => false], + 'Cassandra\\Exception\\UnavailableException::__construct' => ['hasSideEffects' => false], + '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], + 'Collator::getErrorCode' => ['hasSideEffects' => false], + 'Collator::getErrorMessage' => ['hasSideEffects' => false], + 'Collator::getLocale' => ['hasSideEffects' => false], + 'Collator::getSortKey' => ['hasSideEffects' => false], + 'Collator::getStrength' => ['hasSideEffects' => false], + 'DateTime::add' => ['hasSideEffects' => true], + 'DateTime::createFromFormat' => ['hasSideEffects' => false], + 'DateTime::createFromImmutable' => ['hasSideEffects' => false], + 'DateTime::diff' => ['hasSideEffects' => false], + 'DateTime::format' => ['hasSideEffects' => false], + 'DateTime::getLastErrors' => ['hasSideEffects' => false], + 'DateTime::getOffset' => ['hasSideEffects' => false], + 'DateTime::getTimestamp' => ['hasSideEffects' => false], + 'DateTime::getTimezone' => ['hasSideEffects' => false], + 'DateTime::modify' => ['hasSideEffects' => true], + 'DateTime::setDate' => ['hasSideEffects' => true], + 'DateTime::setISODate' => ['hasSideEffects' => true], + 'DateTime::setTime' => ['hasSideEffects' => true], + 'DateTime::setTimestamp' => ['hasSideEffects' => true], + 'DateTime::setTimezone' => ['hasSideEffects' => true], + 'DateTime::sub' => ['hasSideEffects' => true], + 'DateTimeImmutable::add' => ['hasSideEffects' => false], + 'DateTimeImmutable::createFromFormat' => ['hasSideEffects' => false], + 'DateTimeImmutable::createFromMutable' => ['hasSideEffects' => false], + 'DateTimeImmutable::diff' => ['hasSideEffects' => false], + 'DateTimeImmutable::format' => ['hasSideEffects' => false], + 'DateTimeImmutable::getLastErrors' => ['hasSideEffects' => false], + 'DateTimeImmutable::getOffset' => ['hasSideEffects' => false], + 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], + 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], + 'DateTimeImmutable::modify' => ['hasSideEffects' => false], + 'DateTimeImmutable::setDate' => ['hasSideEffects' => false], + 'DateTimeImmutable::setISODate' => ['hasSideEffects' => false], + 'DateTimeImmutable::setTime' => ['hasSideEffects' => false], + 'DateTimeImmutable::setTimestamp' => ['hasSideEffects' => false], + 'DateTimeImmutable::setTimezone' => ['hasSideEffects' => false], + 'DateTimeImmutable::sub' => ['hasSideEffects' => false], + 'Error::__construct' => ['hasSideEffects' => false], + 'ErrorException::__construct' => ['hasSideEffects' => false], + 'Event::__construct' => ['hasSideEffects' => false], + 'EventBase::getFeatures' => ['hasSideEffects' => false], + 'EventBase::getMethod' => ['hasSideEffects' => false], + 'EventBase::getTimeOfDayCached' => ['hasSideEffects' => false], + 'EventBase::gotExit' => ['hasSideEffects' => false], + 'EventBase::gotStop' => ['hasSideEffects' => false], + 'EventBuffer::__construct' => ['hasSideEffects' => false], + 'EventBufferEvent::__construct' => ['hasSideEffects' => false], + 'EventBufferEvent::getDnsErrorString' => ['hasSideEffects' => false], + 'EventBufferEvent::getEnabled' => ['hasSideEffects' => false], + 'EventBufferEvent::getInput' => ['hasSideEffects' => false], + 'EventBufferEvent::getOutput' => ['hasSideEffects' => false], + 'EventConfig::__construct' => ['hasSideEffects' => false], + 'EventDnsBase::__construct' => ['hasSideEffects' => false], + 'EventHttpConnection::__construct' => ['hasSideEffects' => false], + 'EventHttpRequest::__construct' => ['hasSideEffects' => false], + 'EventHttpRequest::getCommand' => ['hasSideEffects' => false], + 'EventHttpRequest::getConnection' => ['hasSideEffects' => false], + 'EventHttpRequest::getHost' => ['hasSideEffects' => false], + 'EventHttpRequest::getInputBuffer' => ['hasSideEffects' => false], + 'EventHttpRequest::getInputHeaders' => ['hasSideEffects' => false], + 'EventHttpRequest::getOutputBuffer' => ['hasSideEffects' => false], + 'EventHttpRequest::getOutputHeaders' => ['hasSideEffects' => false], + 'EventHttpRequest::getResponseCode' => ['hasSideEffects' => false], + 'EventHttpRequest::getUri' => ['hasSideEffects' => false], + 'EventSslContext::__construct' => ['hasSideEffects' => false], + 'Exception::__construct' => ['hasSideEffects' => false], + 'Exception::getCode' => ['hasSideEffects' => false], + 'Exception::getFile' => ['hasSideEffects' => false], + 'Exception::getLine' => ['hasSideEffects' => false], + 'Exception::getMessage' => ['hasSideEffects' => false], + 'Exception::getPrevious' => ['hasSideEffects' => false], + 'Exception::getTrace' => ['hasSideEffects' => false], + 'Exception::getTraceAsString' => ['hasSideEffects' => false], + 'Gmagick::getcopyright' => ['hasSideEffects' => false], + 'Gmagick::getfilename' => ['hasSideEffects' => false], + 'Gmagick::getimagebackgroundcolor' => ['hasSideEffects' => false], + 'Gmagick::getimageblueprimary' => ['hasSideEffects' => false], + 'Gmagick::getimagebordercolor' => ['hasSideEffects' => false], + 'Gmagick::getimagechanneldepth' => ['hasSideEffects' => false], + 'Gmagick::getimagecolors' => ['hasSideEffects' => false], + 'Gmagick::getimagecolorspace' => ['hasSideEffects' => false], + 'Gmagick::getimagecompose' => ['hasSideEffects' => false], + 'Gmagick::getimagedelay' => ['hasSideEffects' => false], + 'Gmagick::getimagedepth' => ['hasSideEffects' => false], + 'Gmagick::getimagedispose' => ['hasSideEffects' => false], + 'Gmagick::getimageextrema' => ['hasSideEffects' => false], + 'Gmagick::getimagefilename' => ['hasSideEffects' => false], + 'Gmagick::getimageformat' => ['hasSideEffects' => false], + 'Gmagick::getimagegamma' => ['hasSideEffects' => false], + 'Gmagick::getimagegreenprimary' => ['hasSideEffects' => false], + 'Gmagick::getimageheight' => ['hasSideEffects' => false], + 'Gmagick::getimagehistogram' => ['hasSideEffects' => false], + 'Gmagick::getimageindex' => ['hasSideEffects' => false], + 'Gmagick::getimageinterlacescheme' => ['hasSideEffects' => false], + 'Gmagick::getimageiterations' => ['hasSideEffects' => false], + 'Gmagick::getimagematte' => ['hasSideEffects' => false], + 'Gmagick::getimagemattecolor' => ['hasSideEffects' => false], + 'Gmagick::getimageprofile' => ['hasSideEffects' => false], + 'Gmagick::getimageredprimary' => ['hasSideEffects' => false], + 'Gmagick::getimagerenderingintent' => ['hasSideEffects' => false], + 'Gmagick::getimageresolution' => ['hasSideEffects' => false], + 'Gmagick::getimagescene' => ['hasSideEffects' => false], + 'Gmagick::getimagesignature' => ['hasSideEffects' => false], + 'Gmagick::getimagetype' => ['hasSideEffects' => false], + 'Gmagick::getimageunits' => ['hasSideEffects' => false], + 'Gmagick::getimagewhitepoint' => ['hasSideEffects' => false], + 'Gmagick::getimagewidth' => ['hasSideEffects' => false], + 'Gmagick::getpackagename' => ['hasSideEffects' => false], + 'Gmagick::getquantumdepth' => ['hasSideEffects' => false], + 'Gmagick::getreleasedate' => ['hasSideEffects' => false], + 'Gmagick::getsamplingfactors' => ['hasSideEffects' => false], + 'Gmagick::getsize' => ['hasSideEffects' => false], + 'Gmagick::getversion' => ['hasSideEffects' => false], + 'GmagickDraw::getfillcolor' => ['hasSideEffects' => false], + 'GmagickDraw::getfillopacity' => ['hasSideEffects' => false], + 'GmagickDraw::getfont' => ['hasSideEffects' => false], + 'GmagickDraw::getfontsize' => ['hasSideEffects' => false], + 'GmagickDraw::getfontstyle' => ['hasSideEffects' => false], + 'GmagickDraw::getfontweight' => ['hasSideEffects' => false], + 'GmagickDraw::getstrokecolor' => ['hasSideEffects' => false], + 'GmagickDraw::getstrokeopacity' => ['hasSideEffects' => false], + 'GmagickDraw::getstrokewidth' => ['hasSideEffects' => false], + 'GmagickDraw::gettextdecoration' => ['hasSideEffects' => false], + 'GmagickDraw::gettextencoding' => ['hasSideEffects' => false], + 'GmagickPixel::getcolor' => ['hasSideEffects' => false], + 'GmagickPixel::getcolorcount' => ['hasSideEffects' => false], + 'GmagickPixel::getcolorvalue' => ['hasSideEffects' => false], + 'HttpMessage::getBody' => ['hasSideEffects' => false], + 'HttpMessage::getHeader' => ['hasSideEffects' => false], + 'HttpMessage::getHeaders' => ['hasSideEffects' => false], + 'HttpMessage::getHttpVersion' => ['hasSideEffects' => false], + 'HttpMessage::getInfo' => ['hasSideEffects' => false], + 'HttpMessage::getParentMessage' => ['hasSideEffects' => false], + 'HttpMessage::getRequestMethod' => ['hasSideEffects' => false], + 'HttpMessage::getRequestUrl' => ['hasSideEffects' => false], + 'HttpMessage::getResponseCode' => ['hasSideEffects' => false], + 'HttpMessage::getResponseStatus' => ['hasSideEffects' => false], + 'HttpMessage::getType' => ['hasSideEffects' => false], + 'HttpQueryString::get' => ['hasSideEffects' => false], + 'HttpQueryString::getArray' => ['hasSideEffects' => false], + 'HttpQueryString::getBool' => ['hasSideEffects' => false], + 'HttpQueryString::getFloat' => ['hasSideEffects' => false], + 'HttpQueryString::getInt' => ['hasSideEffects' => false], + 'HttpQueryString::getObject' => ['hasSideEffects' => false], + 'HttpQueryString::getString' => ['hasSideEffects' => false], + 'HttpRequest::getBody' => ['hasSideEffects' => false], + 'HttpRequest::getContentType' => ['hasSideEffects' => false], + 'HttpRequest::getCookies' => ['hasSideEffects' => false], + 'HttpRequest::getHeaders' => ['hasSideEffects' => false], + 'HttpRequest::getHistory' => ['hasSideEffects' => false], + 'HttpRequest::getMethod' => ['hasSideEffects' => false], + 'HttpRequest::getOptions' => ['hasSideEffects' => false], + 'HttpRequest::getPostFields' => ['hasSideEffects' => false], + 'HttpRequest::getPostFiles' => ['hasSideEffects' => false], + 'HttpRequest::getPutData' => ['hasSideEffects' => false], + 'HttpRequest::getPutFile' => ['hasSideEffects' => false], + 'HttpRequest::getQueryData' => ['hasSideEffects' => false], + 'HttpRequest::getRawPostData' => ['hasSideEffects' => false], + 'HttpRequest::getRawRequestMessage' => ['hasSideEffects' => false], + 'HttpRequest::getRawResponseMessage' => ['hasSideEffects' => false], + 'HttpRequest::getRequestMessage' => ['hasSideEffects' => false], + 'HttpRequest::getResponseBody' => ['hasSideEffects' => false], + 'HttpRequest::getResponseCode' => ['hasSideEffects' => false], + 'HttpRequest::getResponseCookies' => ['hasSideEffects' => false], + 'HttpRequest::getResponseData' => ['hasSideEffects' => false], + 'HttpRequest::getResponseHeader' => ['hasSideEffects' => false], + 'HttpRequest::getResponseInfo' => ['hasSideEffects' => false], + 'HttpRequest::getResponseMessage' => ['hasSideEffects' => false], + 'HttpRequest::getResponseStatus' => ['hasSideEffects' => false], + 'HttpRequest::getSslOptions' => ['hasSideEffects' => false], + 'HttpRequest::getUrl' => ['hasSideEffects' => false], + 'HttpRequestPool::getAttachedRequests' => ['hasSideEffects' => false], + 'HttpRequestPool::getFinishedRequests' => ['hasSideEffects' => false], + 'Imagick::getColorspace' => ['hasSideEffects' => false], + 'Imagick::getCompression' => ['hasSideEffects' => false], + 'Imagick::getCompressionQuality' => ['hasSideEffects' => false], + 'Imagick::getConfigureOptions' => ['hasSideEffects' => false], + 'Imagick::getFeatures' => ['hasSideEffects' => false], + 'Imagick::getFilename' => ['hasSideEffects' => false], + 'Imagick::getFont' => ['hasSideEffects' => false], + 'Imagick::getFormat' => ['hasSideEffects' => false], + 'Imagick::getGravity' => ['hasSideEffects' => false], + 'Imagick::getHDRIEnabled' => ['hasSideEffects' => false], + 'Imagick::getImage' => ['hasSideEffects' => false], + 'Imagick::getImageAlphaChannel' => ['hasSideEffects' => false], + 'Imagick::getImageArtifact' => ['hasSideEffects' => false], + 'Imagick::getImageAttribute' => ['hasSideEffects' => false], + 'Imagick::getImageBackgroundColor' => ['hasSideEffects' => false], + 'Imagick::getImageBlob' => ['hasSideEffects' => false], + 'Imagick::getImageBluePrimary' => ['hasSideEffects' => false], + 'Imagick::getImageBorderColor' => ['hasSideEffects' => false], + 'Imagick::getImageChannelDepth' => ['hasSideEffects' => false], + 'Imagick::getImageChannelDistortion' => ['hasSideEffects' => false], + 'Imagick::getImageChannelDistortions' => ['hasSideEffects' => false], + 'Imagick::getImageChannelExtrema' => ['hasSideEffects' => false], + 'Imagick::getImageChannelKurtosis' => ['hasSideEffects' => false], + 'Imagick::getImageChannelMean' => ['hasSideEffects' => false], + 'Imagick::getImageChannelRange' => ['hasSideEffects' => false], + 'Imagick::getImageChannelStatistics' => ['hasSideEffects' => false], + 'Imagick::getImageClipMask' => ['hasSideEffects' => false], + 'Imagick::getImageColormapColor' => ['hasSideEffects' => false], + 'Imagick::getImageColors' => ['hasSideEffects' => false], + 'Imagick::getImageColorspace' => ['hasSideEffects' => false], + 'Imagick::getImageCompose' => ['hasSideEffects' => false], + 'Imagick::getImageCompression' => ['hasSideEffects' => false], + 'Imagick::getImageCompressionQuality' => ['hasSideEffects' => false], + 'Imagick::getImageDelay' => ['hasSideEffects' => false], + 'Imagick::getImageDepth' => ['hasSideEffects' => false], + 'Imagick::getImageDispose' => ['hasSideEffects' => false], + 'Imagick::getImageDistortion' => ['hasSideEffects' => false], + 'Imagick::getImageExtrema' => ['hasSideEffects' => false], + 'Imagick::getImageFilename' => ['hasSideEffects' => false], + 'Imagick::getImageFormat' => ['hasSideEffects' => false], + 'Imagick::getImageGamma' => ['hasSideEffects' => false], + 'Imagick::getImageGeometry' => ['hasSideEffects' => false], + 'Imagick::getImageGravity' => ['hasSideEffects' => false], + 'Imagick::getImageGreenPrimary' => ['hasSideEffects' => false], + 'Imagick::getImageHeight' => ['hasSideEffects' => false], + 'Imagick::getImageHistogram' => ['hasSideEffects' => false], + 'Imagick::getImageIndex' => ['hasSideEffects' => false], + 'Imagick::getImageInterlaceScheme' => ['hasSideEffects' => false], + 'Imagick::getImageInterpolateMethod' => ['hasSideEffects' => false], + 'Imagick::getImageIterations' => ['hasSideEffects' => false], + 'Imagick::getImageLength' => ['hasSideEffects' => false], + 'Imagick::getImageMatte' => ['hasSideEffects' => false], + 'Imagick::getImageMatteColor' => ['hasSideEffects' => false], + 'Imagick::getImageMimeType' => ['hasSideEffects' => false], + 'Imagick::getImageOrientation' => ['hasSideEffects' => false], + 'Imagick::getImagePage' => ['hasSideEffects' => false], + 'Imagick::getImagePixelColor' => ['hasSideEffects' => false], + 'Imagick::getImageProfile' => ['hasSideEffects' => false], + 'Imagick::getImageProfiles' => ['hasSideEffects' => false], + 'Imagick::getImageProperties' => ['hasSideEffects' => false], + 'Imagick::getImageProperty' => ['hasSideEffects' => false], + 'Imagick::getImageRedPrimary' => ['hasSideEffects' => false], + 'Imagick::getImageRegion' => ['hasSideEffects' => false], + 'Imagick::getImageRenderingIntent' => ['hasSideEffects' => false], + 'Imagick::getImageResolution' => ['hasSideEffects' => false], + 'Imagick::getImageScene' => ['hasSideEffects' => false], + 'Imagick::getImageSignature' => ['hasSideEffects' => false], + 'Imagick::getImageSize' => ['hasSideEffects' => false], + 'Imagick::getImageTicksPerSecond' => ['hasSideEffects' => false], + 'Imagick::getImageTotalInkDensity' => ['hasSideEffects' => false], + 'Imagick::getImageType' => ['hasSideEffects' => false], + 'Imagick::getImageUnits' => ['hasSideEffects' => false], + 'Imagick::getImageVirtualPixelMethod' => ['hasSideEffects' => false], + 'Imagick::getImageWhitePoint' => ['hasSideEffects' => false], + 'Imagick::getImageWidth' => ['hasSideEffects' => false], + 'Imagick::getImagesBlob' => ['hasSideEffects' => false], + 'Imagick::getInterlaceScheme' => ['hasSideEffects' => false], + 'Imagick::getIteratorIndex' => ['hasSideEffects' => false], + 'Imagick::getNumberImages' => ['hasSideEffects' => false], + 'Imagick::getOption' => ['hasSideEffects' => false], + 'Imagick::getPage' => ['hasSideEffects' => false], + 'Imagick::getPixelIterator' => ['hasSideEffects' => false], + 'Imagick::getPixelRegionIterator' => ['hasSideEffects' => false], + 'Imagick::getPointSize' => ['hasSideEffects' => false], + 'Imagick::getSamplingFactors' => ['hasSideEffects' => false], + 'Imagick::getSize' => ['hasSideEffects' => false], + 'Imagick::getSizeOffset' => ['hasSideEffects' => false], + 'ImagickDraw::getBorderColor' => ['hasSideEffects' => false], + 'ImagickDraw::getClipPath' => ['hasSideEffects' => false], + 'ImagickDraw::getClipRule' => ['hasSideEffects' => false], + 'ImagickDraw::getClipUnits' => ['hasSideEffects' => false], + 'ImagickDraw::getDensity' => ['hasSideEffects' => false], + 'ImagickDraw::getFillColor' => ['hasSideEffects' => false], + 'ImagickDraw::getFillOpacity' => ['hasSideEffects' => false], + 'ImagickDraw::getFillRule' => ['hasSideEffects' => false], + 'ImagickDraw::getFont' => ['hasSideEffects' => false], + 'ImagickDraw::getFontFamily' => ['hasSideEffects' => false], + 'ImagickDraw::getFontResolution' => ['hasSideEffects' => false], + 'ImagickDraw::getFontSize' => ['hasSideEffects' => false], + 'ImagickDraw::getFontStretch' => ['hasSideEffects' => false], + 'ImagickDraw::getFontStyle' => ['hasSideEffects' => false], + 'ImagickDraw::getFontWeight' => ['hasSideEffects' => false], + 'ImagickDraw::getGravity' => ['hasSideEffects' => false], + 'ImagickDraw::getOpacity' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeAntialias' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeColor' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeDashArray' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeDashOffset' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeLineCap' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeLineJoin' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeMiterLimit' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeOpacity' => ['hasSideEffects' => false], + 'ImagickDraw::getStrokeWidth' => ['hasSideEffects' => false], + 'ImagickDraw::getTextAlignment' => ['hasSideEffects' => false], + 'ImagickDraw::getTextAntialias' => ['hasSideEffects' => false], + 'ImagickDraw::getTextDecoration' => ['hasSideEffects' => false], + 'ImagickDraw::getTextDirection' => ['hasSideEffects' => false], + 'ImagickDraw::getTextEncoding' => ['hasSideEffects' => false], + 'ImagickDraw::getTextInterLineSpacing' => ['hasSideEffects' => false], + 'ImagickDraw::getTextInterWordSpacing' => ['hasSideEffects' => false], + 'ImagickDraw::getTextKerning' => ['hasSideEffects' => false], + 'ImagickDraw::getTextUnderColor' => ['hasSideEffects' => false], + 'ImagickDraw::getVectorGraphics' => ['hasSideEffects' => false], + 'ImagickKernel::getMatrix' => ['hasSideEffects' => false], + 'ImagickPixel::getColor' => ['hasSideEffects' => false], + 'ImagickPixel::getColorAsString' => ['hasSideEffects' => false], + 'ImagickPixel::getColorCount' => ['hasSideEffects' => false], + 'ImagickPixel::getColorQuantum' => ['hasSideEffects' => false], + 'ImagickPixel::getColorValue' => ['hasSideEffects' => false], + 'ImagickPixel::getColorValueQuantum' => ['hasSideEffects' => false], + 'ImagickPixel::getHSL' => ['hasSideEffects' => false], + 'ImagickPixel::getIndex' => ['hasSideEffects' => false], + 'ImagickPixelIterator::getCurrentIteratorRow' => ['hasSideEffects' => false], + '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], + 'IntlBreakIterator::getIterator' => ['hasSideEffects' => false], + 'IntlBreakIterator::getLocale' => ['hasSideEffects' => false], + 'IntlBreakIterator::getPartsIterator' => ['hasSideEffects' => false], + 'IntlBreakIterator::getText' => ['hasSideEffects' => false], + 'IntlBreakIterator::isBoundary' => ['hasSideEffects' => false], + 'IntlCalendar::after' => ['hasSideEffects' => false], + 'IntlCalendar::before' => ['hasSideEffects' => false], + 'IntlCalendar::equals' => ['hasSideEffects' => false], + 'IntlCalendar::fieldDifference' => ['hasSideEffects' => false], + 'IntlCalendar::get' => ['hasSideEffects' => false], + 'IntlCalendar::getActualMaximum' => ['hasSideEffects' => false], + 'IntlCalendar::getActualMinimum' => ['hasSideEffects' => false], + 'IntlCalendar::getDayOfWeekType' => ['hasSideEffects' => false], + 'IntlCalendar::getErrorCode' => ['hasSideEffects' => false], + 'IntlCalendar::getErrorMessage' => ['hasSideEffects' => false], + 'IntlCalendar::getFirstDayOfWeek' => ['hasSideEffects' => false], + 'IntlCalendar::getGreatestMinimum' => ['hasSideEffects' => false], + 'IntlCalendar::getLeastMaximum' => ['hasSideEffects' => false], + 'IntlCalendar::getLocale' => ['hasSideEffects' => false], + 'IntlCalendar::getMaximum' => ['hasSideEffects' => false], + 'IntlCalendar::getMinimalDaysInFirstWeek' => ['hasSideEffects' => false], + 'IntlCalendar::getMinimum' => ['hasSideEffects' => false], + 'IntlCalendar::getRepeatedWallTimeOption' => ['hasSideEffects' => false], + 'IntlCalendar::getSkippedWallTimeOption' => ['hasSideEffects' => false], + 'IntlCalendar::getTime' => ['hasSideEffects' => false], + 'IntlCalendar::getTimeZone' => ['hasSideEffects' => false], + 'IntlCalendar::getType' => ['hasSideEffects' => false], + 'IntlCalendar::getWeekendTransition' => ['hasSideEffects' => false], + 'IntlCalendar::inDaylightTime' => ['hasSideEffects' => false], + 'IntlCalendar::isEquivalentTo' => ['hasSideEffects' => false], + 'IntlCalendar::isLenient' => ['hasSideEffects' => false], + 'IntlCalendar::isWeekend' => ['hasSideEffects' => false], + 'IntlCalendar::toDateTime' => ['hasSideEffects' => false], + 'IntlChar::hasBinaryProperty' => ['hasSideEffects' => false], + 'IntlCodePointBreakIterator::getLastCodePoint' => ['hasSideEffects' => false], + 'IntlDateFormatter::__construct' => ['hasSideEffects' => false], + 'IntlDateFormatter::getCalendar' => ['hasSideEffects' => false], + 'IntlDateFormatter::getCalendarObject' => ['hasSideEffects' => false], + 'IntlDateFormatter::getDateType' => ['hasSideEffects' => false], + 'IntlDateFormatter::getErrorCode' => ['hasSideEffects' => false], + 'IntlDateFormatter::getErrorMessage' => ['hasSideEffects' => false], + 'IntlDateFormatter::getLocale' => ['hasSideEffects' => false], + 'IntlDateFormatter::getPattern' => ['hasSideEffects' => false], + 'IntlDateFormatter::getTimeType' => ['hasSideEffects' => false], + 'IntlDateFormatter::getTimeZone' => ['hasSideEffects' => false], + 'IntlDateFormatter::getTimeZoneId' => ['hasSideEffects' => false], + 'IntlDateFormatter::isLenient' => ['hasSideEffects' => false], + 'IntlGregorianCalendar::getGregorianChange' => ['hasSideEffects' => false], + 'IntlGregorianCalendar::isLeapYear' => ['hasSideEffects' => false], + 'IntlPartsIterator::getBreakIterator' => ['hasSideEffects' => false], + 'IntlRuleBasedBreakIterator::__construct' => ['hasSideEffects' => false], + 'IntlRuleBasedBreakIterator::getBinaryRules' => ['hasSideEffects' => false], + 'IntlRuleBasedBreakIterator::getRuleStatus' => ['hasSideEffects' => false], + 'IntlRuleBasedBreakIterator::getRuleStatusVec' => ['hasSideEffects' => false], + 'IntlRuleBasedBreakIterator::getRules' => ['hasSideEffects' => false], + 'IntlTimeZone::getDSTSavings' => ['hasSideEffects' => false], + 'IntlTimeZone::getDisplayName' => ['hasSideEffects' => false], + 'IntlTimeZone::getErrorCode' => ['hasSideEffects' => false], + 'IntlTimeZone::getErrorMessage' => ['hasSideEffects' => false], + 'IntlTimeZone::getID' => ['hasSideEffects' => false], + 'IntlTimeZone::getRawOffset' => ['hasSideEffects' => false], + 'IntlTimeZone::hasSameRules' => ['hasSideEffects' => false], + 'IntlTimeZone::toDateTimeZone' => ['hasSideEffects' => false], + 'JsonIncrementalParser::__construct' => ['hasSideEffects' => false], + 'JsonIncrementalParser::get' => ['hasSideEffects' => false], + 'JsonIncrementalParser::getError' => ['hasSideEffects' => false], + 'MemcachedException::__construct' => ['hasSideEffects' => false], + 'MessageFormatter::__construct' => ['hasSideEffects' => false], + 'MessageFormatter::format' => ['hasSideEffects' => false], + 'MessageFormatter::getErrorCode' => ['hasSideEffects' => false], + 'MessageFormatter::getErrorMessage' => ['hasSideEffects' => false], + 'MessageFormatter::getLocale' => ['hasSideEffects' => false], + 'MessageFormatter::getPattern' => ['hasSideEffects' => false], + 'MessageFormatter::parse' => ['hasSideEffects' => false], + 'NumberFormatter::__construct' => ['hasSideEffects' => false], + 'NumberFormatter::format' => ['hasSideEffects' => false], + 'NumberFormatter::formatCurrency' => ['hasSideEffects' => false], + 'NumberFormatter::getAttribute' => ['hasSideEffects' => false], + 'NumberFormatter::getErrorCode' => ['hasSideEffects' => false], + 'NumberFormatter::getErrorMessage' => ['hasSideEffects' => false], + 'NumberFormatter::getLocale' => ['hasSideEffects' => false], + 'NumberFormatter::getPattern' => ['hasSideEffects' => false], + 'NumberFormatter::getSymbol' => ['hasSideEffects' => false], + 'NumberFormatter::getTextAttribute' => ['hasSideEffects' => false], + 'ReflectionAttribute::getArguments' => ['hasSideEffects' => false], + 'ReflectionAttribute::getName' => ['hasSideEffects' => false], + 'ReflectionAttribute::getTarget' => ['hasSideEffects' => false], + 'ReflectionAttribute::isRepeated' => ['hasSideEffects' => false], + 'ReflectionClass::getAttributes' => ['hasSideEffects' => false], + 'ReflectionClass::getConstant' => ['hasSideEffects' => false], + 'ReflectionClass::getConstants' => ['hasSideEffects' => false], + 'ReflectionClass::getConstructor' => ['hasSideEffects' => false], + 'ReflectionClass::getDefaultProperties' => ['hasSideEffects' => false], + 'ReflectionClass::getDocComment' => ['hasSideEffects' => false], + 'ReflectionClass::getEndLine' => ['hasSideEffects' => false], + 'ReflectionClass::getExtension' => ['hasSideEffects' => false], + 'ReflectionClass::getExtensionName' => ['hasSideEffects' => false], + 'ReflectionClass::getFileName' => ['hasSideEffects' => false], + 'ReflectionClass::getInterfaceNames' => ['hasSideEffects' => false], + 'ReflectionClass::getInterfaces' => ['hasSideEffects' => false], + 'ReflectionClass::getMethod' => ['hasSideEffects' => false], + 'ReflectionClass::getMethods' => ['hasSideEffects' => false], + 'ReflectionClass::getModifiers' => ['hasSideEffects' => false], + 'ReflectionClass::getName' => ['hasSideEffects' => false], + 'ReflectionClass::getNamespaceName' => ['hasSideEffects' => false], + 'ReflectionClass::getParentClass' => ['hasSideEffects' => false], + 'ReflectionClass::getProperties' => ['hasSideEffects' => false], + 'ReflectionClass::getProperty' => ['hasSideEffects' => false], + 'ReflectionClass::getReflectionConstant' => ['hasSideEffects' => false], + 'ReflectionClass::getReflectionConstants' => ['hasSideEffects' => false], + 'ReflectionClass::getShortName' => ['hasSideEffects' => false], + 'ReflectionClass::getStartLine' => ['hasSideEffects' => false], + 'ReflectionClass::getStaticProperties' => ['hasSideEffects' => false], + 'ReflectionClass::getStaticPropertyValue' => ['hasSideEffects' => false], + 'ReflectionClass::getTraitAliases' => ['hasSideEffects' => false], + 'ReflectionClass::getTraitNames' => ['hasSideEffects' => false], + 'ReflectionClass::getTraits' => ['hasSideEffects' => false], + 'ReflectionClass::isAbstract' => ['hasSideEffects' => false], + 'ReflectionClass::isAnonymous' => ['hasSideEffects' => false], + 'ReflectionClass::isCloneable' => ['hasSideEffects' => false], + 'ReflectionClass::isFinal' => ['hasSideEffects' => false], + 'ReflectionClass::isInstance' => ['hasSideEffects' => false], + 'ReflectionClass::isInstantiable' => ['hasSideEffects' => false], + 'ReflectionClass::isInterface' => ['hasSideEffects' => false], + '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], + 'ReflectionClassConstant::getAttributes' => ['hasSideEffects' => false], + 'ReflectionClassConstant::getDeclaringClass' => ['hasSideEffects' => false], + 'ReflectionClassConstant::getDocComment' => ['hasSideEffects' => false], + 'ReflectionClassConstant::getModifiers' => ['hasSideEffects' => false], + 'ReflectionClassConstant::getName' => ['hasSideEffects' => false], + 'ReflectionClassConstant::getValue' => ['hasSideEffects' => false], + '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], + 'ReflectionExtension::getDependencies' => ['hasSideEffects' => false], + 'ReflectionExtension::getFunctions' => ['hasSideEffects' => false], + 'ReflectionExtension::getINIEntries' => ['hasSideEffects' => false], + 'ReflectionExtension::getName' => ['hasSideEffects' => false], + 'ReflectionExtension::getVersion' => ['hasSideEffects' => false], + 'ReflectionExtension::isPersistent' => ['hasSideEffects' => false], + 'ReflectionExtension::isTemporary' => ['hasSideEffects' => false], + '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], + 'ReflectionFunctionAbstract::getDocComment' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getEndLine' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getExtension' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getExtensionName' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getFileName' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getName' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getNamespaceName' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getNumberOfParameters' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getNumberOfRequiredParameters' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getParameters' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getReturnType' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getShortName' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getStartLine' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getStaticVariables' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getTentativeReturnType' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::hasTentativeReturnType' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isClosure' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isDeprecated' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isGenerator' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isInternal' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isStatic' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isUserDefined' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isVariadic' => ['hasSideEffects' => false], + 'ReflectionGenerator::getExecutingFile' => ['hasSideEffects' => false], + 'ReflectionGenerator::getExecutingGenerator' => ['hasSideEffects' => false], + 'ReflectionGenerator::getExecutingLine' => ['hasSideEffects' => false], + 'ReflectionGenerator::getFunction' => ['hasSideEffects' => false], + 'ReflectionGenerator::getThis' => ['hasSideEffects' => false], + 'ReflectionGenerator::getTrace' => ['hasSideEffects' => false], + 'ReflectionIntersectionType::getTypes' => ['hasSideEffects' => false], + 'ReflectionMethod::getClosure' => ['hasSideEffects' => false], + 'ReflectionMethod::getDeclaringClass' => ['hasSideEffects' => false], + 'ReflectionMethod::getModifiers' => ['hasSideEffects' => false], + 'ReflectionMethod::getPrototype' => ['hasSideEffects' => false], + 'ReflectionMethod::isAbstract' => ['hasSideEffects' => false], + 'ReflectionMethod::isConstructor' => ['hasSideEffects' => false], + 'ReflectionMethod::isDestructor' => ['hasSideEffects' => false], + 'ReflectionMethod::isFinal' => ['hasSideEffects' => false], + 'ReflectionMethod::isPrivate' => ['hasSideEffects' => false], + 'ReflectionMethod::isProtected' => ['hasSideEffects' => false], + 'ReflectionMethod::isPublic' => ['hasSideEffects' => false], + 'ReflectionMethod::isStatic' => ['hasSideEffects' => false], + 'ReflectionMethod::setAccessible' => ['hasSideEffects' => false], + 'ReflectionNamedType::getName' => ['hasSideEffects' => false], + 'ReflectionNamedType::isBuiltin' => ['hasSideEffects' => false], + 'ReflectionParameter::getAttributes' => ['hasSideEffects' => false], + 'ReflectionParameter::getClass' => ['hasSideEffects' => false], + 'ReflectionParameter::getDeclaringClass' => ['hasSideEffects' => false], + 'ReflectionParameter::getDeclaringFunction' => ['hasSideEffects' => false], + 'ReflectionParameter::getDefaultValue' => ['hasSideEffects' => false], + 'ReflectionParameter::getDefaultValueConstantName' => ['hasSideEffects' => false], + 'ReflectionParameter::getName' => ['hasSideEffects' => false], + 'ReflectionParameter::getPosition' => ['hasSideEffects' => false], + 'ReflectionParameter::getType' => ['hasSideEffects' => false], + 'ReflectionParameter::isArray' => ['hasSideEffects' => false], + 'ReflectionParameter::isCallable' => ['hasSideEffects' => false], + 'ReflectionParameter::isDefaultValueAvailable' => ['hasSideEffects' => false], + 'ReflectionParameter::isDefaultValueConstant' => ['hasSideEffects' => false], + 'ReflectionParameter::isOptional' => ['hasSideEffects' => false], + 'ReflectionParameter::isPassedByReference' => ['hasSideEffects' => false], + 'ReflectionParameter::isPromoted' => ['hasSideEffects' => false], + 'ReflectionParameter::isVariadic' => ['hasSideEffects' => false], + 'ReflectionProperty::getAttributes' => ['hasSideEffects' => false], + 'ReflectionProperty::getDeclaringClass' => ['hasSideEffects' => false], + 'ReflectionProperty::getDefaultValue' => ['hasSideEffects' => false], + 'ReflectionProperty::getDocComment' => ['hasSideEffects' => false], + 'ReflectionProperty::getModifiers' => ['hasSideEffects' => false], + 'ReflectionProperty::getName' => ['hasSideEffects' => false], + 'ReflectionProperty::getType' => ['hasSideEffects' => false], + 'ReflectionProperty::getValue' => ['hasSideEffects' => false], + 'ReflectionProperty::isDefault' => ['hasSideEffects' => false], + 'ReflectionProperty::isInitialized' => ['hasSideEffects' => false], + 'ReflectionProperty::isPrivate' => ['hasSideEffects' => false], + 'ReflectionProperty::isPromoted' => ['hasSideEffects' => false], + 'ReflectionProperty::isProtected' => ['hasSideEffects' => false], + 'ReflectionProperty::isPublic' => ['hasSideEffects' => false], + 'ReflectionProperty::isStatic' => ['hasSideEffects' => false], + 'ReflectionProperty::setAccessible' => ['hasSideEffects' => false], + 'ReflectionReference::getId' => ['hasSideEffects' => false], + 'ReflectionType::isBuiltin' => ['hasSideEffects' => false], + 'ReflectionUnionType::getTypes' => ['hasSideEffects' => false], + 'ReflectionZendExtension::getAuthor' => ['hasSideEffects' => false], + 'ReflectionZendExtension::getCopyright' => ['hasSideEffects' => false], + 'ReflectionZendExtension::getName' => ['hasSideEffects' => false], + 'ReflectionZendExtension::getURL' => ['hasSideEffects' => false], + 'ReflectionZendExtension::getVersion' => ['hasSideEffects' => false], + 'ResourceBundle::__construct' => ['hasSideEffects' => false], + 'ResourceBundle::count' => ['hasSideEffects' => false], + 'ResourceBundle::get' => ['hasSideEffects' => false], + 'ResourceBundle::getErrorCode' => ['hasSideEffects' => false], + 'ResourceBundle::getErrorMessage' => ['hasSideEffects' => false], + 'ResourceBundle::getIterator' => ['hasSideEffects' => false], + 'SQLiteException::__construct' => ['hasSideEffects' => false], + 'SimpleXMLElement::__construct' => ['hasSideEffects' => false], + 'SimpleXMLElement::children' => ['hasSideEffects' => false], + 'SimpleXMLElement::count' => ['hasSideEffects' => false], + 'SimpleXMLElement::current' => ['hasSideEffects' => false], + 'SimpleXMLElement::getChildren' => ['hasSideEffects' => false], + 'SimpleXMLElement::getDocNamespaces' => ['hasSideEffects' => false], + 'SimpleXMLElement::getName' => ['hasSideEffects' => false], + 'SimpleXMLElement::getNamespaces' => ['hasSideEffects' => false], + 'SimpleXMLElement::hasChildren' => ['hasSideEffects' => false], + 'SimpleXMLElement::offsetExists' => ['hasSideEffects' => false], + 'SimpleXMLElement::offsetGet' => ['hasSideEffects' => false], + 'SimpleXMLElement::valid' => ['hasSideEffects' => false], + 'SimpleXMLIterator::count' => ['hasSideEffects' => false], + 'SimpleXMLIterator::current' => ['hasSideEffects' => false], + 'SimpleXMLIterator::getChildren' => ['hasSideEffects' => false], + '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\\StubsMetaExpectedArgumentsTest::getClassMemberFqn' => ['hasSideEffects' => false], + 'StubTests\\StubsParameterNamesTest::printParameters' => ['hasSideEffects' => false], + 'Transliterator::createInverse' => ['hasSideEffects' => false], + 'Transliterator::getErrorCode' => ['hasSideEffects' => false], + 'Transliterator::getErrorMessage' => ['hasSideEffects' => false], + 'Transliterator::transliterate' => ['hasSideEffects' => false], + 'UConverter::__construct' => ['hasSideEffects' => false], + 'UConverter::convert' => ['hasSideEffects' => false], + 'UConverter::getDestinationEncoding' => ['hasSideEffects' => false], + 'UConverter::getDestinationType' => ['hasSideEffects' => false], + 'UConverter::getErrorCode' => ['hasSideEffects' => false], + 'UConverter::getErrorMessage' => ['hasSideEffects' => false], + 'UConverter::getSourceEncoding' => ['hasSideEffects' => false], + 'UConverter::getSourceType' => ['hasSideEffects' => false], + '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], + 'Zookeeper::getRecvTimeout' => ['hasSideEffects' => false], + 'Zookeeper::getState' => ['hasSideEffects' => false], + '_' => ['hasSideEffects' => false], + 'abs' => ['hasSideEffects' => false], + 'acos' => ['hasSideEffects' => false], + 'acosh' => ['hasSideEffects' => false], + 'addcslashes' => ['hasSideEffects' => false], + 'addslashes' => ['hasSideEffects' => false], + 'apache_get_modules' => ['hasSideEffects' => false], + 'apache_get_version' => ['hasSideEffects' => false], + 'apache_getenv' => ['hasSideEffects' => false], + 'apache_request_headers' => ['hasSideEffects' => false], + 'array_change_key_case' => ['hasSideEffects' => false], + 'array_chunk' => ['hasSideEffects' => false], + 'array_column' => ['hasSideEffects' => false], + 'array_combine' => ['hasSideEffects' => false], + 'array_count_values' => ['hasSideEffects' => false], + 'array_diff' => ['hasSideEffects' => false], + 'array_diff_assoc' => ['hasSideEffects' => false], + 'array_diff_key' => ['hasSideEffects' => false], + 'array_diff_uassoc' => ['hasSideEffects' => false], + 'array_diff_ukey' => ['hasSideEffects' => false], + 'array_fill' => ['hasSideEffects' => false], + 'array_fill_keys' => ['hasSideEffects' => false], + 'array_flip' => ['hasSideEffects' => false], + 'array_intersect' => ['hasSideEffects' => false], + 'array_intersect_assoc' => ['hasSideEffects' => false], + 'array_intersect_key' => ['hasSideEffects' => false], + 'array_intersect_uassoc' => ['hasSideEffects' => false], + 'array_intersect_ukey' => ['hasSideEffects' => false], + 'array_is_list' => ['hasSideEffects' => false], + 'array_key_exists' => ['hasSideEffects' => false], + 'array_key_first' => ['hasSideEffects' => false], + 'array_key_last' => ['hasSideEffects' => false], + 'array_keys' => ['hasSideEffects' => false], + '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], + 'array_udiff_assoc' => ['hasSideEffects' => false], + 'array_udiff_uassoc' => ['hasSideEffects' => false], + 'array_uintersect' => ['hasSideEffects' => false], + '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], + 'atan' => ['hasSideEffects' => false], + 'atan2' => ['hasSideEffects' => false], + 'atanh' => ['hasSideEffects' => false], + 'base64_decode' => ['hasSideEffects' => false], + 'base64_encode' => ['hasSideEffects' => false], + 'base_convert' => ['hasSideEffects' => false], + 'basename' => ['hasSideEffects' => false], + 'bcadd' => ['hasSideEffects' => false], + 'bccomp' => ['hasSideEffects' => false], + 'bcdiv' => ['hasSideEffects' => false], + 'bcmod' => ['hasSideEffects' => false], + 'bcmul' => ['hasSideEffects' => false], + 'bcpow' => ['hasSideEffects' => false], + 'bcpowmod' => ['hasSideEffects' => false], + 'bcsqrt' => ['hasSideEffects' => false], + 'bcsub' => ['hasSideEffects' => false], + 'bin2hex' => ['hasSideEffects' => false], + 'bindec' => ['hasSideEffects' => false], + 'boolval' => ['hasSideEffects' => false], + 'bzcompress' => ['hasSideEffects' => false], + 'bzdecompress' => ['hasSideEffects' => false], + 'bzerrno' => ['hasSideEffects' => false], + 'bzerror' => ['hasSideEffects' => false], + 'bzerrstr' => ['hasSideEffects' => false], + 'bzopen' => ['hasSideEffects' => false], + 'ceil' => ['hasSideEffects' => false], + 'checkdate' => ['hasSideEffects' => false], + 'checkdnsrr' => ['hasSideEffects' => false], + 'chgrp' => ['hasSideEffects' => true], + 'chmod' => ['hasSideEffects' => true], + 'chop' => ['hasSideEffects' => false], + 'chown' => ['hasSideEffects' => true], + 'chr' => ['hasSideEffects' => false], + 'chunk_split' => ['hasSideEffects' => false], + 'class_implements' => ['hasSideEffects' => false], + 'class_parents' => ['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' => 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' => true], + 'connection_status' => ['hasSideEffects' => true], + 'constant' => ['hasSideEffects' => true], + 'convert_cyr_string' => ['hasSideEffects' => false], + 'convert_uudecode' => ['hasSideEffects' => false], + 'convert_uuencode' => ['hasSideEffects' => false], + 'copy' => ['hasSideEffects' => true], + 'cos' => ['hasSideEffects' => false], + 'cosh' => ['hasSideEffects' => false], + 'count' => ['hasSideEffects' => false], + 'count_chars' => ['hasSideEffects' => false], + 'crc32' => ['hasSideEffects' => false], + 'crypt' => ['hasSideEffects' => false], + 'ctype_alnum' => ['hasSideEffects' => false], + 'ctype_alpha' => ['hasSideEffects' => false], + 'ctype_cntrl' => ['hasSideEffects' => false], + 'ctype_digit' => ['hasSideEffects' => false], + 'ctype_graph' => ['hasSideEffects' => false], + 'ctype_lower' => ['hasSideEffects' => false], + 'ctype_print' => ['hasSideEffects' => false], + 'ctype_punct' => ['hasSideEffects' => false], + 'ctype_space' => ['hasSideEffects' => false], + 'ctype_upper' => ['hasSideEffects' => false], + 'ctype_xdigit' => ['hasSideEffects' => false], + 'curl_copy_handle' => ['hasSideEffects' => false], + 'curl_errno' => ['hasSideEffects' => true], + 'curl_error' => ['hasSideEffects' => true], + 'curl_escape' => ['hasSideEffects' => false], + 'curl_file_create' => ['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' => true], + 'curl_share_strerror' => ['hasSideEffects' => false], + 'curl_strerror' => ['hasSideEffects' => false], + 'curl_unescape' => ['hasSideEffects' => false], + 'curl_version' => ['hasSideEffects' => false], + 'current' => ['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' => 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' => true], + 'datefmt_get_error_message' => ['hasSideEffects' => true], + 'datefmt_get_locale' => ['hasSideEffects' => false], + 'datefmt_get_pattern' => ['hasSideEffects' => false], + 'datefmt_get_timetype' => ['hasSideEffects' => false], + 'datefmt_get_timezone' => ['hasSideEffects' => false], + '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' => true], + 'deflate_init' => ['hasSideEffects' => false], + 'deg2rad' => ['hasSideEffects' => false], + 'dirname' => ['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' => true], + 'error_log' => ['hasSideEffects' => true], + 'escapeshellarg' => ['hasSideEffects' => false], + 'escapeshellcmd' => ['hasSideEffects' => false], + 'exp' => ['hasSideEffects' => false], + 'explode' => ['hasSideEffects' => false], + 'expm1' => ['hasSideEffects' => false], + 'extension_loaded' => ['hasSideEffects' => false], + 'fclose' => ['hasSideEffects' => true], + 'fdiv' => ['hasSideEffects' => false], + 'feof' => ['hasSideEffects' => true], + 'fflush' => ['hasSideEffects' => true], + 'fgetc' => ['hasSideEffects' => true], + 'fgetcsv' => ['hasSideEffects' => true], + 'fgets' => ['hasSideEffects' => true], + 'fgetss' => ['hasSideEffects' => true], + 'file' => ['hasSideEffects' => true], + 'file_exists' => ['hasSideEffects' => false], + 'file_get_contents' => ['hasSideEffects' => true], + 'file_put_contents' => ['hasSideEffects' => true], + 'fileatime' => ['hasSideEffects' => false], + 'filectime' => ['hasSideEffects' => false], + 'filegroup' => ['hasSideEffects' => false], + 'fileinode' => ['hasSideEffects' => false], + 'filemtime' => ['hasSideEffects' => false], + 'fileowner' => ['hasSideEffects' => false], + 'fileperms' => ['hasSideEffects' => false], + 'filesize' => ['hasSideEffects' => false], + 'filetype' => ['hasSideEffects' => false], + 'filter_has_var' => ['hasSideEffects' => false], + 'filter_id' => ['hasSideEffects' => false], + 'filter_input' => ['hasSideEffects' => false], + 'filter_input_array' => ['hasSideEffects' => false], + 'filter_list' => ['hasSideEffects' => false], + 'filter_var' => ['hasSideEffects' => false], + 'filter_var_array' => ['hasSideEffects' => false], + 'finfo::buffer' => ['hasSideEffects' => false], + 'finfo::file' => ['hasSideEffects' => false], + 'floatval' => ['hasSideEffects' => false], + 'flock' => ['hasSideEffects' => true], + 'floor' => ['hasSideEffects' => false], + 'fmod' => ['hasSideEffects' => false], + 'fnmatch' => ['hasSideEffects' => true], + 'fopen' => ['hasSideEffects' => true], + 'fpassthru' => ['hasSideEffects' => true], + 'fputcsv' => ['hasSideEffects' => true], + 'fputs' => ['hasSideEffects' => true], + 'fread' => ['hasSideEffects' => true], + 'fscanf' => ['hasSideEffects' => true], + 'fseek' => ['hasSideEffects' => true], + 'fstat' => ['hasSideEffects' => true], + 'ftell' => ['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' => true], + 'gc_status' => ['hasSideEffects' => true], + 'gd_info' => ['hasSideEffects' => false], + 'geoip_continent_code_by_name' => ['hasSideEffects' => false], + 'geoip_country_code3_by_name' => ['hasSideEffects' => false], + 'geoip_country_code_by_name' => ['hasSideEffects' => false], + 'geoip_country_name_by_name' => ['hasSideEffects' => false], + 'geoip_database_info' => ['hasSideEffects' => false], + 'geoip_db_avail' => ['hasSideEffects' => false], + 'geoip_db_filename' => ['hasSideEffects' => false], + 'geoip_db_get_all_info' => ['hasSideEffects' => false], + 'geoip_id_by_name' => ['hasSideEffects' => false], + 'geoip_isp_by_name' => ['hasSideEffects' => false], + 'geoip_org_by_name' => ['hasSideEffects' => false], + 'geoip_record_by_name' => ['hasSideEffects' => false], + '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' => 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' => true], + 'get_debug_type' => ['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' => true], + 'get_html_translation_table' => ['hasSideEffects' => false], + 'get_include_path' => ['hasSideEffects' => true], + 'get_included_files' => ['hasSideEffects' => true], + 'get_loaded_extensions' => ['hasSideEffects' => false], + 'get_meta_tags' => ['hasSideEffects' => true], + 'get_object_vars' => ['hasSideEffects' => true], + 'get_parent_class' => ['hasSideEffects' => false], + 'get_required_files' => ['hasSideEffects' => true], + 'get_resource_id' => ['hasSideEffects' => false], + 'get_resource_type' => ['hasSideEffects' => true], + 'get_resources' => ['hasSideEffects' => true], + 'getallheaders' => ['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' => 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' => true], + 'getservbyname' => ['hasSideEffects' => false], + 'getservbyport' => ['hasSideEffects' => false], + 'gettext' => ['hasSideEffects' => false], + 'gettimeofday' => ['hasSideEffects' => true], + 'gettype' => ['hasSideEffects' => false], + 'glob' => ['hasSideEffects' => true], + 'gmdate' => ['hasSideEffects' => true], + 'gmmktime' => ['hasSideEffects' => true], + 'gmp_abs' => ['hasSideEffects' => false], + 'gmp_add' => ['hasSideEffects' => false], + 'gmp_and' => ['hasSideEffects' => false], + 'gmp_binomial' => ['hasSideEffects' => false], + 'gmp_cmp' => ['hasSideEffects' => false], + 'gmp_com' => ['hasSideEffects' => false], + 'gmp_div' => ['hasSideEffects' => false], + 'gmp_div_q' => ['hasSideEffects' => false], + 'gmp_div_qr' => ['hasSideEffects' => false], + 'gmp_div_r' => ['hasSideEffects' => false], + 'gmp_divexact' => ['hasSideEffects' => false], + 'gmp_export' => ['hasSideEffects' => false], + 'gmp_fact' => ['hasSideEffects' => false], + 'gmp_gcd' => ['hasSideEffects' => false], + 'gmp_gcdext' => ['hasSideEffects' => false], + 'gmp_hamdist' => ['hasSideEffects' => false], + 'gmp_import' => ['hasSideEffects' => false], + 'gmp_init' => ['hasSideEffects' => false], + 'gmp_intval' => ['hasSideEffects' => false], + 'gmp_invert' => ['hasSideEffects' => false], + 'gmp_jacobi' => ['hasSideEffects' => false], + 'gmp_kronecker' => ['hasSideEffects' => false], + 'gmp_lcm' => ['hasSideEffects' => false], + 'gmp_legendre' => ['hasSideEffects' => false], + 'gmp_mod' => ['hasSideEffects' => false], + 'gmp_mul' => ['hasSideEffects' => false], + 'gmp_neg' => ['hasSideEffects' => false], + 'gmp_nextprime' => ['hasSideEffects' => false], + 'gmp_or' => ['hasSideEffects' => false], + 'gmp_perfect_power' => ['hasSideEffects' => false], + 'gmp_perfect_square' => ['hasSideEffects' => false], + 'gmp_popcount' => ['hasSideEffects' => false], + 'gmp_pow' => ['hasSideEffects' => false], + 'gmp_powm' => ['hasSideEffects' => false], + 'gmp_prob_prime' => ['hasSideEffects' => false], + 'gmp_root' => ['hasSideEffects' => false], + 'gmp_rootrem' => ['hasSideEffects' => false], + 'gmp_scan0' => ['hasSideEffects' => false], + 'gmp_scan1' => ['hasSideEffects' => false], + 'gmp_sign' => ['hasSideEffects' => false], + 'gmp_sqrt' => ['hasSideEffects' => false], + 'gmp_sqrtrem' => ['hasSideEffects' => false], + 'gmp_strval' => ['hasSideEffects' => false], + 'gmp_sub' => ['hasSideEffects' => false], + 'gmp_testbit' => ['hasSideEffects' => false], + 'gmp_xor' => ['hasSideEffects' => false], + 'grapheme_stripos' => ['hasSideEffects' => false], + 'grapheme_stristr' => ['hasSideEffects' => false], + 'grapheme_strlen' => ['hasSideEffects' => false], + 'grapheme_strpos' => ['hasSideEffects' => false], + 'grapheme_strripos' => ['hasSideEffects' => false], + 'grapheme_strrpos' => ['hasSideEffects' => false], + 'grapheme_strstr' => ['hasSideEffects' => false], + 'grapheme_substr' => ['hasSideEffects' => false], + 'gzcompress' => ['hasSideEffects' => false], + 'gzdecode' => ['hasSideEffects' => false], + 'gzdeflate' => ['hasSideEffects' => false], + 'gzencode' => ['hasSideEffects' => false], + 'gzinflate' => ['hasSideEffects' => false], + 'gzuncompress' => ['hasSideEffects' => false], + 'hash' => ['hasSideEffects' => false], + 'hash_algos' => ['hasSideEffects' => false], + 'hash_copy' => ['hasSideEffects' => false], + 'hash_equals' => ['hasSideEffects' => false], + 'hash_file' => ['hasSideEffects' => false], + 'hash_hkdf' => ['hasSideEffects' => false], + 'hash_hmac' => ['hasSideEffects' => false], + 'hash_hmac_algos' => ['hasSideEffects' => false], + 'hash_hmac_file' => ['hasSideEffects' => false], + 'hash_init' => ['hasSideEffects' => false], + 'hash_pbkdf2' => ['hasSideEffects' => false], + 'headers_list' => ['hasSideEffects' => false], + 'hebrev' => ['hasSideEffects' => false], + 'hexdec' => ['hasSideEffects' => false], + 'hrtime' => ['hasSideEffects' => true], + 'html_entity_decode' => ['hasSideEffects' => false], + 'htmlentities' => ['hasSideEffects' => false], + 'htmlspecialchars' => ['hasSideEffects' => false], + 'htmlspecialchars_decode' => ['hasSideEffects' => false], + 'http_build_cookie' => ['hasSideEffects' => false], + 'http_build_query' => ['hasSideEffects' => false], + 'http_build_str' => ['hasSideEffects' => false], + 'http_cache_etag' => ['hasSideEffects' => false], + 'http_cache_last_modified' => ['hasSideEffects' => false], + 'http_chunked_decode' => ['hasSideEffects' => false], + 'http_date' => ['hasSideEffects' => false], + 'http_deflate' => ['hasSideEffects' => false], + 'http_get_request_body' => ['hasSideEffects' => false], + 'http_get_request_body_stream' => ['hasSideEffects' => false], + 'http_get_request_headers' => ['hasSideEffects' => false], + 'http_inflate' => ['hasSideEffects' => false], + 'http_match_etag' => ['hasSideEffects' => false], + 'http_match_modified' => ['hasSideEffects' => false], + 'http_match_request_header' => ['hasSideEffects' => false], + 'http_parse_cookie' => ['hasSideEffects' => false], + 'http_parse_headers' => ['hasSideEffects' => false], + 'http_parse_message' => ['hasSideEffects' => false], + 'http_parse_params' => ['hasSideEffects' => false], + 'http_request_body_encode' => ['hasSideEffects' => false], + 'http_request_method_exists' => ['hasSideEffects' => false], + 'http_request_method_name' => ['hasSideEffects' => false], + 'http_support' => ['hasSideEffects' => false], + 'hypot' => ['hasSideEffects' => false], + 'iconv' => ['hasSideEffects' => false], + 'iconv_get_encoding' => ['hasSideEffects' => false], + 'iconv_mime_decode' => ['hasSideEffects' => false], + 'iconv_mime_decode_headers' => ['hasSideEffects' => false], + 'iconv_mime_encode' => ['hasSideEffects' => false], + 'iconv_strlen' => ['hasSideEffects' => false], + 'iconv_strpos' => ['hasSideEffects' => false], + 'iconv_strrpos' => ['hasSideEffects' => false], + 'iconv_substr' => ['hasSideEffects' => false], + 'idate' => ['hasSideEffects' => true], + 'image_type_to_extension' => ['hasSideEffects' => false], + 'image_type_to_mime_type' => ['hasSideEffects' => false], + 'imagecolorat' => ['hasSideEffects' => false], + 'imagecolorclosest' => ['hasSideEffects' => false], + 'imagecolorclosestalpha' => ['hasSideEffects' => false], + 'imagecolorclosesthwb' => ['hasSideEffects' => false], + 'imagecolorexact' => ['hasSideEffects' => false], + 'imagecolorexactalpha' => ['hasSideEffects' => false], + 'imagecolorresolve' => ['hasSideEffects' => false], + 'imagecolorresolvealpha' => ['hasSideEffects' => false], + 'imagecolorsforindex' => ['hasSideEffects' => false], + 'imagecolorstotal' => ['hasSideEffects' => false], + 'imagecreate' => ['hasSideEffects' => false], + 'imagecreatefromstring' => ['hasSideEffects' => false], + 'imagecreatetruecolor' => ['hasSideEffects' => false], + 'imagefontheight' => ['hasSideEffects' => false], + 'imagefontwidth' => ['hasSideEffects' => false], + 'imageftbbox' => ['hasSideEffects' => false], + 'imagegetinterpolation' => ['hasSideEffects' => false], + 'imagegrabscreen' => ['hasSideEffects' => false], + 'imagegrabwindow' => ['hasSideEffects' => false], + 'imageistruecolor' => ['hasSideEffects' => false], + 'imagesx' => ['hasSideEffects' => false], + 'imagesy' => ['hasSideEffects' => false], + 'imagettfbbox' => ['hasSideEffects' => false], + 'imagetypes' => ['hasSideEffects' => false], + 'implode' => ['hasSideEffects' => false], + 'in_array' => ['hasSideEffects' => false], + 'inet_ntop' => ['hasSideEffects' => false], + 'inet_pton' => ['hasSideEffects' => false], + 'inflate_get_read_len' => ['hasSideEffects' => false], + 'inflate_get_status' => ['hasSideEffects' => false], + 'inflate_init' => ['hasSideEffects' => false], + 'ini_get' => ['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' => true], + 'intl_get_error_message' => ['hasSideEffects' => true], + 'intl_is_failure' => ['hasSideEffects' => false], + 'intlcal_after' => ['hasSideEffects' => false], + 'intlcal_before' => ['hasSideEffects' => false], + 'intlcal_create_instance' => ['hasSideEffects' => false], + 'intlcal_equals' => ['hasSideEffects' => false], + 'intlcal_field_difference' => ['hasSideEffects' => false], + 'intlcal_from_date_time' => ['hasSideEffects' => false], + 'intlcal_get' => ['hasSideEffects' => false], + 'intlcal_get_actual_maximum' => ['hasSideEffects' => false], + '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' => 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], + 'intlcal_get_least_maximum' => ['hasSideEffects' => false], + 'intlcal_get_locale' => ['hasSideEffects' => false], + 'intlcal_get_maximum' => ['hasSideEffects' => false], + 'intlcal_get_minimal_days_in_first_week' => ['hasSideEffects' => false], + 'intlcal_get_minimum' => ['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], + 'intlcal_get_time_zone' => ['hasSideEffects' => false], + 'intlcal_get_type' => ['hasSideEffects' => false], + 'intlcal_get_weekend_transition' => ['hasSideEffects' => false], + 'intlcal_greates_minimum' => ['hasSideEffects' => false], + 'intlcal_in_daylight_time' => ['hasSideEffects' => false], + 'intlcal_is_equivalent_to' => ['hasSideEffects' => false], + 'intlcal_is_lenient' => ['hasSideEffects' => false], + 'intlcal_is_set' => ['hasSideEffects' => false], + 'intlcal_is_weekend' => ['hasSideEffects' => false], + 'intlcal_to_date_time' => ['hasSideEffects' => false], + 'intlgregcal_create_instance' => ['hasSideEffects' => false], + 'intlgregcal_get_gregorian_change' => ['hasSideEffects' => false], + 'intlgregcal_is_leap_year' => ['hasSideEffects' => false], + 'intltz_count_equivalent_ids' => ['hasSideEffects' => false], + 'intltz_create_default' => ['hasSideEffects' => false], + 'intltz_create_enumeration' => ['hasSideEffects' => false], + 'intltz_create_time_zone' => ['hasSideEffects' => false], + 'intltz_create_time_zone_id_enumeration' => ['hasSideEffects' => false], + 'intltz_from_date_time_zone' => ['hasSideEffects' => false], + 'intltz_get_canonical_id' => ['hasSideEffects' => false], + 'intltz_get_display_name' => ['hasSideEffects' => false], + 'intltz_get_dst_savings' => ['hasSideEffects' => false], + 'intltz_get_equivalent_id' => ['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], + 'intltz_get_raw_offset' => ['hasSideEffects' => false], + 'intltz_get_region' => ['hasSideEffects' => false], + 'intltz_get_tz_data_version' => ['hasSideEffects' => false], + 'intltz_get_unknown' => ['hasSideEffects' => false], + 'intltz_getgmt' => ['hasSideEffects' => false], + 'intltz_has_same_rules' => ['hasSideEffects' => false], + 'intltz_to_date_time_zone' => ['hasSideEffects' => false], + 'intltz_use_daylight_time' => ['hasSideEffects' => false], + 'intlz_create_default' => ['hasSideEffects' => false], + 'intval' => ['hasSideEffects' => false], + 'ip2long' => ['hasSideEffects' => false], + 'iptcparse' => ['hasSideEffects' => false], + 'is_a' => ['hasSideEffects' => false], + 'is_array' => ['hasSideEffects' => false], + 'is_bool' => ['hasSideEffects' => false], + 'is_countable' => ['hasSideEffects' => false], + 'is_dir' => ['hasSideEffects' => false], + 'is_double' => ['hasSideEffects' => false], + 'is_executable' => ['hasSideEffects' => false], + 'is_file' => ['hasSideEffects' => false], + 'is_finite' => ['hasSideEffects' => false], + 'is_float' => ['hasSideEffects' => false], + 'is_infinite' => ['hasSideEffects' => false], + 'is_int' => ['hasSideEffects' => false], + 'is_integer' => ['hasSideEffects' => false], + 'is_iterable' => ['hasSideEffects' => false], + 'is_link' => ['hasSideEffects' => false], + 'is_long' => ['hasSideEffects' => false], + 'is_nan' => ['hasSideEffects' => false], + 'is_null' => ['hasSideEffects' => false], + 'is_numeric' => ['hasSideEffects' => false], + 'is_object' => ['hasSideEffects' => false], + 'is_readable' => ['hasSideEffects' => false], + 'is_real' => ['hasSideEffects' => false], + 'is_resource' => ['hasSideEffects' => false], + 'is_scalar' => ['hasSideEffects' => false], + 'is_string' => ['hasSideEffects' => false], + 'is_subclass_of' => ['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' => true], + 'libxml_get_last_error' => ['hasSideEffects' => true], + 'link' => ['hasSideEffects' => true], + 'linkinfo' => ['hasSideEffects' => true], + 'locale_accept_from_http' => ['hasSideEffects' => false], + 'locale_canonicalize' => ['hasSideEffects' => false], + 'locale_compose' => ['hasSideEffects' => false], + 'locale_filter_matches' => ['hasSideEffects' => false], + 'locale_get_all_variants' => ['hasSideEffects' => false], + 'locale_get_default' => ['hasSideEffects' => false], + 'locale_get_display_language' => ['hasSideEffects' => false], + 'locale_get_display_name' => ['hasSideEffects' => false], + 'locale_get_display_region' => ['hasSideEffects' => false], + 'locale_get_display_script' => ['hasSideEffects' => false], + 'locale_get_display_variant' => ['hasSideEffects' => false], + 'locale_get_keywords' => ['hasSideEffects' => false], + 'locale_get_primary_language' => ['hasSideEffects' => false], + 'locale_get_region' => ['hasSideEffects' => false], + 'locale_get_script' => ['hasSideEffects' => false], + 'locale_lookup' => ['hasSideEffects' => false], + 'locale_parse' => ['hasSideEffects' => false], + 'localeconv' => ['hasSideEffects' => true], + 'localtime' => ['hasSideEffects' => true], + 'log' => ['hasSideEffects' => false], + 'log10' => ['hasSideEffects' => false], + 'log1p' => ['hasSideEffects' => false], + 'long2ip' => ['hasSideEffects' => false], + 'lstat' => ['hasSideEffects' => false], + 'ltrim' => ['hasSideEffects' => false], + 'max' => ['hasSideEffects' => false], + 'mb_check_encoding' => ['hasSideEffects' => false], + 'mb_chr' => ['hasSideEffects' => false], + 'mb_convert_case' => ['hasSideEffects' => false], + 'mb_convert_encoding' => ['hasSideEffects' => false], + 'mb_convert_kana' => ['hasSideEffects' => false], + 'mb_decode_mimeheader' => ['hasSideEffects' => false], + 'mb_decode_numericentity' => ['hasSideEffects' => false], + 'mb_detect_encoding' => ['hasSideEffects' => false], + 'mb_encode_mimeheader' => ['hasSideEffects' => false], + 'mb_encode_numericentity' => ['hasSideEffects' => false], + 'mb_encoding_aliases' => ['hasSideEffects' => false], + 'mb_ereg_match' => ['hasSideEffects' => false], + 'mb_ereg_replace' => ['hasSideEffects' => false], + 'mb_ereg_search' => ['hasSideEffects' => false], + 'mb_ereg_search_getpos' => ['hasSideEffects' => false], + 'mb_ereg_search_getregs' => ['hasSideEffects' => false], + 'mb_ereg_search_pos' => ['hasSideEffects' => false], + 'mb_ereg_search_regs' => ['hasSideEffects' => false], + 'mb_ereg_search_setpos' => ['hasSideEffects' => false], + 'mb_eregi_replace' => ['hasSideEffects' => false], + 'mb_get_info' => ['hasSideEffects' => false], + 'mb_http_input' => ['hasSideEffects' => false], + 'mb_list_encodings' => ['hasSideEffects' => false], + 'mb_ord' => ['hasSideEffects' => false], + 'mb_output_handler' => ['hasSideEffects' => false], + '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], + 'mb_stripos' => ['hasSideEffects' => false], + 'mb_stristr' => ['hasSideEffects' => false], + 'mb_strlen' => ['hasSideEffects' => false], + 'mb_strpos' => ['hasSideEffects' => false], + 'mb_strrchr' => ['hasSideEffects' => false], + 'mb_strrichr' => ['hasSideEffects' => false], + 'mb_strripos' => ['hasSideEffects' => false], + 'mb_strrpos' => ['hasSideEffects' => false], + 'mb_strstr' => ['hasSideEffects' => false], + 'mb_strtolower' => ['hasSideEffects' => false], + 'mb_strtoupper' => ['hasSideEffects' => false], + 'mb_strwidth' => ['hasSideEffects' => false], + 'mb_substr' => ['hasSideEffects' => false], + 'mb_substr_count' => ['hasSideEffects' => false], + 'mbereg_search_setpos' => ['hasSideEffects' => false], + 'md5' => ['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], + 'mhash_count' => ['hasSideEffects' => false], + 'mhash_get_block_size' => ['hasSideEffects' => false], + 'mhash_get_hash_name' => ['hasSideEffects' => false], + 'mhash_keygen_s2k' => ['hasSideEffects' => false], + 'microtime' => ['hasSideEffects' => true], + 'min' => ['hasSideEffects' => false], + 'mkdir' => ['hasSideEffects' => true], + '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' => true], + 'msgfmt_get_error_message' => ['hasSideEffects' => true], + 'msgfmt_get_locale' => ['hasSideEffects' => false], + 'msgfmt_get_pattern' => ['hasSideEffects' => false], + 'msgfmt_parse' => ['hasSideEffects' => false], + 'msgfmt_parse_message' => ['hasSideEffects' => false], + 'mt_getrandmax' => ['hasSideEffects' => false], + 'mt_rand' => ['hasSideEffects' => true], + 'net_get_interfaces' => ['hasSideEffects' => false], + 'ngettext' => ['hasSideEffects' => false], + 'nl2br' => ['hasSideEffects' => false], + 'nl_langinfo' => ['hasSideEffects' => true], + 'normalizer_get_raw_decomposition' => ['hasSideEffects' => false], + 'normalizer_is_normalized' => ['hasSideEffects' => false], + 'normalizer_normalize' => ['hasSideEffects' => false], + 'number_format' => ['hasSideEffects' => false], + 'numfmt_create' => ['hasSideEffects' => false], + 'numfmt_format' => ['hasSideEffects' => false], + 'numfmt_format_currency' => ['hasSideEffects' => false], + 'numfmt_get_attribute' => ['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_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], + 'pam_auth' => ['hasSideEffects' => false], + 'pam_chpass' => ['hasSideEffects' => false], + 'parse_ini_file' => ['hasSideEffects' => true], + 'parse_ini_string' => ['hasSideEffects' => false], + 'parse_url' => ['hasSideEffects' => false], + 'pathinfo' => ['hasSideEffects' => true], + 'pclose' => ['hasSideEffects' => true], + 'pcntl_errno' => ['hasSideEffects' => true], + 'pcntl_get_last_error' => ['hasSideEffects' => true], + 'pcntl_getpriority' => ['hasSideEffects' => false], + 'pcntl_strerror' => ['hasSideEffects' => false], + 'pcntl_wexitstatus' => ['hasSideEffects' => false], + 'pcntl_wifcontinued' => ['hasSideEffects' => false], + 'pcntl_wifexited' => ['hasSideEffects' => false], + 'pcntl_wifsignaled' => ['hasSideEffects' => false], + 'pcntl_wifstopped' => ['hasSideEffects' => false], + 'pcntl_wstopsig' => ['hasSideEffects' => false], + 'pcntl_wtermsig' => ['hasSideEffects' => false], + 'pdo_drivers' => ['hasSideEffects' => false], + 'php_ini_loaded_file' => ['hasSideEffects' => false], + 'php_ini_scanned_files' => ['hasSideEffects' => false], + 'php_logo_guid' => ['hasSideEffects' => false], + 'php_sapi_name' => ['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' => true], + 'posix_get_last_error' => ['hasSideEffects' => true], + 'posix_getcwd' => ['hasSideEffects' => true], + 'posix_getegid' => ['hasSideEffects' => false], + 'posix_geteuid' => ['hasSideEffects' => false], + 'posix_getgid' => ['hasSideEffects' => false], + 'posix_getgrgid' => ['hasSideEffects' => false], + 'posix_getgrnam' => ['hasSideEffects' => false], + 'posix_getgroups' => ['hasSideEffects' => false], + 'posix_getlogin' => ['hasSideEffects' => false], + 'posix_getpgid' => ['hasSideEffects' => false], + 'posix_getpgrp' => ['hasSideEffects' => false], + 'posix_getpid' => ['hasSideEffects' => false], + 'posix_getppid' => ['hasSideEffects' => false], + 'posix_getpwnam' => ['hasSideEffects' => false], + 'posix_getpwuid' => ['hasSideEffects' => false], + 'posix_getrlimit' => ['hasSideEffects' => false], + 'posix_getsid' => ['hasSideEffects' => false], + 'posix_getuid' => ['hasSideEffects' => false], + 'posix_initgroups' => ['hasSideEffects' => false], + 'posix_isatty' => ['hasSideEffects' => false], + 'posix_strerror' => ['hasSideEffects' => false], + 'posix_times' => ['hasSideEffects' => false], + 'posix_ttyname' => ['hasSideEffects' => false], + 'posix_uname' => ['hasSideEffects' => false], + 'pow' => ['hasSideEffects' => false], + 'preg_grep' => ['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], + 'quoted_printable_decode' => ['hasSideEffects' => false], + 'quoted_printable_encode' => ['hasSideEffects' => false], + 'quotemeta' => ['hasSideEffects' => false], + 'rad2deg' => ['hasSideEffects' => false], + 'rand' => ['hasSideEffects' => true], + 'random_bytes' => ['hasSideEffects' => true], + 'random_int' => ['hasSideEffects' => true], + 'range' => ['hasSideEffects' => false], + 'rawurldecode' => ['hasSideEffects' => false], + 'rawurlencode' => ['hasSideEffects' => false], + 'readfile' => ['hasSideEffects' => true], + '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' => 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' => true], + 'sin' => ['hasSideEffects' => false], + 'sinh' => ['hasSideEffects' => false], + 'sizeof' => ['hasSideEffects' => false], + 'soundex' => ['hasSideEffects' => false], + 'spl_classes' => ['hasSideEffects' => false], + 'spl_object_hash' => ['hasSideEffects' => false], + 'sprintf' => ['hasSideEffects' => false], + '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], + 'str_split' => ['hasSideEffects' => false], + 'str_starts_with' => ['hasSideEffects' => false], + 'str_word_count' => ['hasSideEffects' => false], + 'strcasecmp' => ['hasSideEffects' => false], + 'strchr' => ['hasSideEffects' => false], + 'strcmp' => ['hasSideEffects' => false], + 'strcoll' => ['hasSideEffects' => false], + 'strcspn' => ['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], + 'stripcslashes' => ['hasSideEffects' => false], + 'stripos' => ['hasSideEffects' => false], + 'stripslashes' => ['hasSideEffects' => false], + 'stristr' => ['hasSideEffects' => false], + 'strlen' => ['hasSideEffects' => false], + 'strnatcasecmp' => ['hasSideEffects' => false], + 'strnatcmp' => ['hasSideEffects' => false], + 'strncasecmp' => ['hasSideEffects' => false], + 'strncmp' => ['hasSideEffects' => false], + 'strpbrk' => ['hasSideEffects' => false], + 'strpos' => ['hasSideEffects' => false], + 'strptime' => ['hasSideEffects' => true], + 'strrchr' => ['hasSideEffects' => false], + 'strrev' => ['hasSideEffects' => false], + 'strripos' => ['hasSideEffects' => false], + 'strrpos' => ['hasSideEffects' => false], + 'strspn' => ['hasSideEffects' => false], + 'strstr' => ['hasSideEffects' => false], + 'strtolower' => ['hasSideEffects' => false], + 'strtotime' => ['hasSideEffects' => true], + 'strtoupper' => ['hasSideEffects' => false], + 'strtr' => ['hasSideEffects' => false], + 'strval' => ['hasSideEffects' => false], + 'substr' => ['hasSideEffects' => false], + 'substr_compare' => ['hasSideEffects' => false], + 'substr_count' => ['hasSideEffects' => false], + 'substr_replace' => ['hasSideEffects' => false], + 'symlink' => ['hasSideEffects' => true], + 'sys_getloadavg' => ['hasSideEffects' => true], + 'tan' => ['hasSideEffects' => false], + 'tanh' => ['hasSideEffects' => false], + 'tempnam' => ['hasSideEffects' => true], + 'timezone_abbreviations_list' => ['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' => true], + 'timezone_open' => ['hasSideEffects' => true], + 'timezone_transitions_get' => ['hasSideEffects' => true], + 'timezone_version_get' => ['hasSideEffects' => false], + 'tmpfile' => ['hasSideEffects' => true], + 'token_get_all' => ['hasSideEffects' => false], + 'token_name' => ['hasSideEffects' => false], + 'touch' => ['hasSideEffects' => true], + 'transliterator_create' => ['hasSideEffects' => false], + 'transliterator_create_from_rules' => ['hasSideEffects' => false], + 'transliterator_create_inverse' => ['hasSideEffects' => false], + '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], + 'xml_get_current_byte_index' => ['hasSideEffects' => false], + 'xml_get_current_column_number' => ['hasSideEffects' => false], + 'xml_get_current_line_number' => ['hasSideEffects' => false], + 'xml_get_error_code' => ['hasSideEffects' => false], + 'xml_parser_create' => ['hasSideEffects' => false], + 'xml_parser_create_ns' => ['hasSideEffects' => false], + 'xml_parser_get_option' => ['hasSideEffects' => false], + 'zend_version' => ['hasSideEffects' => false], + 'zlib_decode' => ['hasSideEffects' => false], + 'zlib_encode' => ['hasSideEffects' => false], + 'zlib_get_coding_type' => ['hasSideEffects' => false], + +]; \ No newline at end of file diff --git a/src/AnalysedCodeException.php b/src/AnalysedCodeException.php index d4e7558622..6d78350e50 100644 --- a/src/AnalysedCodeException.php +++ b/src/AnalysedCodeException.php @@ -2,7 +2,9 @@ namespace PHPStan; -abstract class AnalysedCodeException extends \Exception +use Exception; + +abstract class AnalysedCodeException extends Exception { abstract public function getTip(): ?string; diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 28a8d88885..31599aaee4 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -2,49 +2,44 @@ namespace PHPStan\Analyser; -use PHPStan\Rules\Registry; - -class Analyser +use Closure; +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 memory_get_peak_usage; + +/** + * @phpstan-import-type CollectorData from CollectedData + */ +final class Analyser { - private \PHPStan\Analyser\FileAnalyser $fileAnalyser; - - private Registry $registry; - - private \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver; - - private int $internalErrorsCountLimit; - - /** @var \PHPStan\Analyser\Error[] */ - private array $collectedErrors = []; - public function __construct( - FileAnalyser $fileAnalyser, - Registry $registry, - NodeScopeResolver $nodeScopeResolver, - int $internalErrorsCountLimit + private FileAnalyser $fileAnalyser, + private RuleRegistry $ruleRegistry, + private CollectorRegistry $collectorRegistry, + private NodeScopeResolver $nodeScopeResolver, + private int $internalErrorsCountLimit, ) { - $this->fileAnalyser = $fileAnalyser; - $this->registry = $registry; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->internalErrorsCountLimit = $internalErrorsCountLimit; } /** * @param string[] $files - * @param \Closure(string $file): void|null $preFileCallback - * @param \Closure(int): void|null $postFileCallback - * @param bool $debug + * @param Closure(string $file): void|null $preFileCallback + * @param Closure(int ): void|null $postFileCallback * @param string[]|null $allAnalysedFiles - * @return AnalyserResult */ public function analyse( array $files, - ?\Closure $preFileCallback = null, - ?\Closure $postFileCallback = null, + ?Closure $preFileCallback = null, + ?Closure $postFileCallback = null, bool $debug = false, - ?array $allAnalysedFiles = null + ?array $allAnalysedFiles = null, ): AnalyserResult { if ($allAnalysedFiles === null) { @@ -54,12 +49,26 @@ public function analyse( $this->nodeScopeResolver->setAnalysedFiles($allAnalysedFiles); $allAnalysedFiles = array_fill_keys($allAnalysedFiles, true); - $this->collectErrors($files); - + /** @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 = []; + $exportedNodes = []; foreach ($files as $file) { if ($preFileCallback !== null) { $preFileCallback($file); @@ -69,24 +78,35 @@ public function analyse( $fileAnalyserResult = $this->fileAnalyser->analyseFile( $file, $allAnalysedFiles, - $this->registry, - null + $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(); - } catch (\Throwable $t) { + + $fileExportedNodes = $fileAnalyserResult->getExportedNodes(); + if (count($fileExportedNodes) > 0) { + $exportedNodes[$file] = $fileExportedNodes; + } + } catch (Throwable $t) { if ($debug) { 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' - ); - $errors[] = new Error($internalErrorMessage, $file, null, false); + $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; @@ -100,43 +120,20 @@ public function analyse( $postFileCallback(1); } - $this->restoreCollectErrorsHandler(); - - $errors = array_merge($errors, $this->collectedErrors); - return new AnalyserResult( $errors, + $filteredPhpErrors, + $allPhpErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, [], + $collectedData, $internalErrorsCount === 0 ? $dependencies : null, - $reachedInternalErrorsCountLimit + $exportedNodes, + $reachedInternalErrorsCountLimit, + memory_get_peak_usage(true), ); } - /** - * @param string[] $analysedFiles - */ - private function collectErrors(array $analysedFiles): void - { - $this->collectedErrors = []; - set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($analysedFiles): bool { - if (error_reporting() === 0) { - // silence @ operator - return true; - } - - if (!in_array($errfile, $analysedFiles, true)) { - return true; - } - - $this->collectedErrors[] = new Error($errstr, $errfile, $errline, true); - - return true; - }); - } - - private function restoreCollectErrorsHandler(): void - { - restore_error_handler(); - } - } diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index e0f7eb4d98..4226e76fbd 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -2,42 +2,67 @@ namespace PHPStan\Analyser; -class AnalyserResult +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; +use function usort; + +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData + */ +final class AnalyserResult { - /** @var \PHPStan\Analyser\Error[] */ - private array $unorderedErrors; - - /** @var \PHPStan\Analyser\Error[] */ - private array $errors; - - /** @var string[] */ - private array $internalErrors; - - /** @var array>|null */ - private ?array $dependencies; - - private bool $reachedInternalErrorsCountLimit; + /** @var list|null */ + private ?array $errors = null; /** - * @param \PHPStan\Analyser\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 bool $reachedInternalErrorsCountLimit + * @param array> $exportedNodes */ public function __construct( - array $errors, - array $internalErrors, - ?array $dependencies, - bool $reachedInternalErrorsCountLimit + 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( - $errors, - static function (Error $a, Error $b): int { - return [ + /** + * @return list + */ + public function getUnorderedErrors(): array + { + return $this->unorderedErrors; + } + + /** + * @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(), @@ -45,40 +70,69 @@ static function (Error $a, Error $b): int { $b->getFile(), $b->getLine(), $b->getMessage(), - ]; - } - ); - - $this->errors = $errors; - $this->internalErrors = $internalErrors; - $this->dependencies = $dependencies; - $this->reachedInternalErrorsCountLimit = $reachedInternalErrorsCountLimit; + ], + ); + } + + return $this->errors; } /** - * @return \PHPStan\Analyser\Error[] + * @return list */ - public function getUnorderedErrors(): array + public function getFilteredPhpErrors(): array { - return $this->unorderedErrors; + return $this->filteredPhpErrors; } /** - * @return \PHPStan\Analyser\Error[] + * @return list */ - public function getErrors(): array + public function getAllPhpErrors(): array { - return $this->errors; + return $this->allPhpErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; } /** - * @return string[] + * @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 */ @@ -87,9 +141,22 @@ public function getDependencies(): ?array return $this->dependencies; } + /** + * @return array> + */ + public function getExportedNodes(): array + { + return $this->exportedNodes; + } + 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 new file mode 100644 index 0000000000..6907183966 --- /dev/null +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -0,0 +1,55 @@ + $conditionExpressionTypeHolders + */ + public function __construct( + private array $conditionExpressionTypeHolders, + private ExpressionTypeHolder $typeHolder, + ) + { + if (count($conditionExpressionTypeHolders) === 0) { + throw new ShouldNotHappenException(); + } + } + + /** + * @return array + */ + public function getConditionExpressionTypeHolders(): array + { + return $this->conditionExpressionTypeHolders; + } + + public function getTypeHolder(): ExpressionTypeHolder + { + return $this->typeHolder; + } + + public function getKey(): string + { + $parts = []; + foreach ($this->conditionExpressionTypeHolders as $exprString => $typeHolder) { + $parts[] = $exprString . '=' . $typeHolder->getType()->describe(VerbosityLevel::precise()); + } + + return sprintf( + '%s => %s (%s)', + implode(' && ', $parts), + $this->typeHolder->getType()->describe(VerbosityLevel::precise()), + $this->typeHolder->getCertainty()->describe(), + ); + } + +} 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 10f0e9b36a..0000000000 --- a/src/Analyser/DirectScopeFactory.php +++ /dev/null @@ -1,130 +0,0 @@ -scopeClass = $scopeClass; - $this->reflectionProvider = $reflectionProvider; - $this->dynamicReturnTypeExtensionRegistryProvider = $dynamicReturnTypeExtensionRegistryProvider; - $this->operatorTypeSpecifyingExtensionRegistryProvider = $operatorTypeSpecifyingExtensionRegistryProvider; - $this->printer = $printer; - $this->typeSpecifier = $typeSpecifier; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->parser = $parser; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->dynamicConstantNames = $container->getParameter('dynamicConstantNames'); - } - - /** - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack - * - * @return MutatingScope - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [] - ): MutatingScope - { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return new $scopeClass( - $this, - $this->reflectionProvider, - $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), - $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry(), - $this->printer, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $context, - $declareStrictTypes, - $constantTypes, - $function, - $namespace, - $variablesTypes, - $moreSpecificTypes, - $inClosureBindScopeClass, - $anonymousFunctionReflection, - $inFirstLevelStatement, - $currentlyAssignedExpressions, - $nativeExpressionTypes, - $inFunctionCallsStack, - $this->dynamicConstantNames, - $this->treatPhpDocTypesAsCertain - ); - } - -} 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 a949f46976..6a9e539cf1 100644 --- a/src/Analyser/EnsuredNonNullabilityResult.php +++ b/src/Analyser/EnsuredNonNullabilityResult.php @@ -2,22 +2,14 @@ namespace PHPStan\Analyser; -class EnsuredNonNullabilityResult +final class EnsuredNonNullabilityResult { - private MutatingScope $scope; - - /** @var EnsuredNonNullabilityResultExpression[] */ - private array $specifiedExpressions; - /** - * @param MutatingScope $scope * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions */ - public function __construct(MutatingScope $scope, array $specifiedExpressions) + public function __construct(private MutatingScope $scope, private array $specifiedExpressions) { - $this->scope = $scope; - $this->specifiedExpressions = $specifiedExpressions; } public function getScope(): MutatingScope diff --git a/src/Analyser/EnsuredNonNullabilityResultExpression.php b/src/Analyser/EnsuredNonNullabilityResultExpression.php index 9956f0f2f8..59f5eba2ee 100644 --- a/src/Analyser/EnsuredNonNullabilityResultExpression.php +++ b/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -3,19 +3,19 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class EnsuredNonNullabilityResultExpression +final class EnsuredNonNullabilityResultExpression { - private Expr $expression; - - private Type $originalType; - - public function __construct(Expr $expression, Type $originalType) + public function __construct( + private Expr $expression, + private Type $originalType, + private Type $originalNativeType, + private TrinaryLogic $certainty, + ) { - $this->expression = $expression; - $this->originalType = $originalType; } public function getExpression(): Expr @@ -28,4 +28,14 @@ public function getOriginalType(): Type return $this->originalType; } + 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 a7a754a67e..1ad85c60be 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -2,73 +2,47 @@ namespace PHPStan\Analyser; -class Error implements \JsonSerializable +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 + */ +final class Error implements JsonSerializable { - private string $message; - - private string $file; - - private ?int $line; - - private bool $canBeIgnored; - - private ?string $filePath; - - private ?string $traitFilePath; - - private ?string $tip; - - private ?int $nodeLine; - - /** @phpstan-var class-string<\PhpParser\Node>|null */ - private ?string $nodeType; - - private ?string $identifier; - - /** @var mixed[] */ - private array $metadata; + public const PATTERN_IDENTIFIER = '[a-zA-Z0-9](?:[a-zA-Z0-9\\.]*[a-zA-Z0-9])?'; /** * Error constructor. * - * @param string $message - * @param string $file - * @param int|null $line - * @param bool $canBeIgnored - * @param string|null $filePath - * @param string|null $traitFilePath - * @param string|null $tip - * @param int|null $nodeLine - * @param class-string<\PhpParser\Node>|null $nodeType - * @param string|null $identifier + * @param class-string|null $nodeType * @param mixed[] $metadata */ public function __construct( - string $message, - string $file, - ?int $line = null, - bool $canBeIgnored = true, - ?string $filePath = null, - ?string $traitFilePath = null, - ?string $tip = null, - ?int $nodeLine = null, - ?string $nodeType = null, - ?string $identifier = null, - array $metadata = [] + private string $message, + private string $file, + private ?int $line = null, + private bool|Throwable $canBeIgnored = true, + private ?string $filePath = null, + private ?string $traitFilePath = null, + private ?string $tip = null, + private ?int $nodeLine = null, + private ?string $nodeType = null, + private ?string $identifier = null, + private array $metadata = [], ) { - $this->message = $message; - $this->file = $file; - $this->line = $line; - $this->canBeIgnored = $canBeIgnored; - $this->filePath = $filePath; - $this->traitFilePath = $traitFilePath; - $this->tip = $tip; - $this->nodeLine = $nodeLine; - $this->nodeType = $nodeType; - $this->identifier = $identifier; - $this->metadata = $metadata; + if ($this->identifier !== null && !self::validateIdentifier($this->identifier)) { + throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $this->identifier)); + } } public function getMessage(): string @@ -90,6 +64,44 @@ public function getFilePath(): string return $this->filePath; } + public function changeFilePath(string $newFilePath): self + { + if ($this->traitFilePath !== null) { + throw new ShouldNotHappenException('Errors in traits not yet supported'); + } + + return new self( + $this->message, + $newFilePath, + $this->line, + $this->canBeIgnored, + $newFilePath, + null, + $this->tip, + $this->nodeLine, + $this->nodeType, + $this->identifier, + $this->metadata, + ); + } + + public function changeTraitFilePath(string $newFilePath): self + { + return new self( + $this->message, + $this->file, + $this->line, + $this->canBeIgnored, + $this->filePath, + $newFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, + $this->identifier, + $this->metadata, + ); + } + public function getTraitFilePath(): ?string { return $this->traitFilePath; @@ -102,7 +114,12 @@ public function getLine(): ?int public function canBeIgnored(): bool { - return $this->canBeIgnored; + return $this->canBeIgnored === true; + } + + public function hasNonIgnorableException(): bool + { + return $this->canBeIgnored instanceof Throwable; } public function getTip(): ?string @@ -125,7 +142,71 @@ public function withoutTip(): self $this->traitFilePath, null, $this->nodeLine, - $this->nodeType + $this->nodeType, + ); + } + + public function doNotIgnore(): self + { + if (!$this->canBeIgnored()) { + return $this; + } + + return new self( + $this->message, + $this->file, + $this->line, + false, + $this->filePath, + $this->traitFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, + ); + } + + 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, ); } @@ -135,13 +216,18 @@ public function getNodeLine(): ?int } /** - * @return class-string<\PhpParser\Node>|null + * @return class-string|null */ 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; @@ -158,13 +244,14 @@ public function getMetadata(): array /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ 'message' => $this->message, 'file' => $this->file, 'line' => $this->line, - 'canBeIgnored' => $this->canBeIgnored, + 'canBeIgnored' => is_bool($this->canBeIgnored) ? $this->canBeIgnored : 'exception', 'filePath' => $this->filePath, 'traitFilePath' => $this->traitFilePath, 'tip' => $this->tip, @@ -177,7 +264,6 @@ public function jsonSerialize() /** * @param mixed[] $json - * @return self */ public static function decode(array $json): self { @@ -185,20 +271,19 @@ public static function decode(array $json): self $json['message'], $json['file'], $json['line'], - $json['canBeIgnored'], + $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'], $json['filePath'], $json['traitFilePath'], $json['tip'], $json['nodeLine'] ?? null, $json['nodeType'] ?? null, $json['identifier'] ?? null, - $json['metadata'] ?? [] + $json['metadata'] ?? [], ); } /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { @@ -213,8 +298,13 @@ public static function __set_state(array $properties): self $properties['nodeLine'] ?? null, $properties['nodeType'] ?? null, $properties['identifier'] ?? null, - $properties['metadata'] ?? [] + $properties['metadata'] ?? [], ); } + 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 373ea50d0a..c910b0cc3d 100644 --- a/src/Analyser/ExpressionContext.php +++ b/src/Analyser/ExpressionContext.php @@ -4,34 +4,26 @@ use PHPStan\Type\Type; -class ExpressionContext +final class ExpressionContext { - private bool $isDeep; - - private ?string $inAssignRightSideVariableName; - - private ?Type $inAssignRightSideType; - private function __construct( - bool $isDeep, - ?string $inAssignRightSideVariableName, - ?Type $inAssignRightSideType + private bool $isDeep, + private ?string $inAssignRightSideVariableName, + private ?Type $inAssignRightSideType, + private ?Type $inAssignRightSideNativeType, ) { - $this->isDeep = $isDeep; - $this->inAssignRightSideVariableName = $inAssignRightSideVariableName; - $this->inAssignRightSideType = $inAssignRightSideType; } 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 @@ -40,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 @@ -48,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 @@ -63,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 baa35933d0..0a50465169 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,13 +2,9 @@ namespace PHPStan\Analyser; -class ExpressionResult +final class ExpressionResult { - private MutatingScope $scope; - - private bool $hasYield; - /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -20,20 +16,20 @@ class ExpressionResult private ?MutatingScope $falseyScope = null; /** - * @param MutatingScope $scope - * @param bool $hasYield + * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ public function __construct( - MutatingScope $scope, - bool $hasYield, + private MutatingScope $scope, + private bool $hasYield, + private array $throwPoints, + private array $impurePoints, ?callable $truthyScopeCallback = null, - ?callable $falseyScopeCallback = null + ?callable $falseyScopeCallback = null, ) { - $this->scope = $scope; - $this->hasYield = $hasYield; $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; } @@ -48,6 +44,22 @@ public function hasYield(): bool return $this->hasYield; } + /** + * @return ThrowPoint[] + */ + 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 f2f39c2556..80724ea18a 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -2,177 +2,218 @@ namespace PHPStan\Analyser; -use PhpParser\Comment; use PhpParser\Node; +use PHPStan\AnalysedCodeException; +use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; +use PHPStan\BetterReflection\Reflection\Exception\CircularReference; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\Collectors\CollectedData; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; -use PHPStan\File\FileHelper; use PHPStan\Node\FileNode; +use PHPStan\Node\InTraitNode; use PHPStan\Parser\Parser; -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 Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; -use function array_fill_keys; -use function array_key_exists; +use PHPStan\Parser\ParserErrorsException; +use PHPStan\Rules\Registry as RuleRegistry; +use function array_keys; use function array_unique; - -class FileAnalyser +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 restore_error_handler; +use function set_error_handler; +use function sprintf; +use const E_DEPRECATED; +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 { - private \PHPStan\Analyser\ScopeFactory $scopeFactory; - - private \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Parser\Parser $parser; - - private DependencyResolver $dependencyResolver; - - private FileHelper $fileHelper; + /** @var list */ + private array $allPhpErrors = []; - private bool $reportUnmatchedIgnoredErrors; + /** @var list */ + private array $filteredPhpErrors = []; public function __construct( - ScopeFactory $scopeFactory, - NodeScopeResolver $nodeScopeResolver, - Parser $parser, - DependencyResolver $dependencyResolver, - FileHelper $fileHelper, - bool $reportUnmatchedIgnoredErrors + private ScopeFactory $scopeFactory, + private NodeScopeResolver $nodeScopeResolver, + private Parser $parser, + private DependencyResolver $dependencyResolver, + private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, + private RuleErrorTransformer $ruleErrorTransformer, + private LocalIgnoresProcessor $localIgnoresProcessor, ) { - $this->scopeFactory = $scopeFactory; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->parser = $parser; - $this->dependencyResolver = $dependencyResolver; - $this->fileHelper = $fileHelper; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; } /** - * @param string $file * @param array $analysedFiles - * @param Registry $registry - * @param callable(\PhpParser\Node $node, Scope $scope): void|null $outerNodeCallback - * @return FileAnalyserResult + * @param callable(Node $node, Scope $scope): void|null $outerNodeCallback */ public function analyseFile( string $file, array $analysedFiles, - Registry $registry, - ?callable $outerNodeCallback + 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 = []; + $linesToIgnore = $unmatchedLineIgnores = [$file => $this->getLinesToIgnoreFromTokens($parserNodes)]; $temporaryFileErrors = []; - $nodeCallback = function (\PhpParser\Node $node, Scope $scope) use (&$fileErrors, &$fileDependencies, $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($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 (\PHPStan\AnalysedCodeException $e) { + } catch (AnalysedCodeException $e) { if (isset($uniquedAnalysedCodeExceptionMessages[$e->getMessage()])) { continue; } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = new Error($e->getMessage(), $file, $node->getLine(), false, 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(), false); + $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; } 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() !== false) { - $traitFilePath = $traitReflection->getFileName(); + $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; + } } } - 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(); - } + $temporaryFileErrors[] = $error; + } + } - if ($ruleError instanceof IdentifierRuleError) { - $identifier = $ruleError->getIdentifier(); - } + foreach ($collectorRegistry->getCollectors($nodeType) as $collector) { + try { + $collectedData = $collector->processNode($node, $scope); + } catch (AnalysedCodeException $e) { + if (isset($uniquedAnalysedCodeExceptionMessages[$e->getMessage()])) { + continue; + } - if ($ruleError instanceof MetadataRuleError) { - $metadata = $ruleError->getMetadata(); - } + $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 ($ruleError instanceof NonIgnorableRuleError) { - $canBeIgnored = false; - } - } - $temporaryFileErrors[] = new Error( - $message, - $fileName, - $line, - $canBeIgnored, - $filePath, - $traitFilePath, - $tip, - $nodeLine, - $nodeType, - $identifier, - $metadata - ); + if ($collectedData === null) { + continue; } - } - foreach ($this->getLinesToIgnore($node) as $lineToIgnore) { - $linesToIgnore[] = $lineToIgnore; + $fileCollectedData[$scope->getFile()][get_class($collector)][] = $collectedData; } try { - foreach ($this->resolveDependencies($node, $scope, $analysedFiles) as $dependentFile) { + $dependencies = $this->dependencyResolver->resolveDependencies($node, $scope); + foreach ($dependencies->getFileDependencies($scope->getFile(), $analysedFiles) as $dependentFile) { $fileDependencies[] = $dependentFile; } - } catch (\PHPStan\AnalysedCodeException $e) { + if ($dependencies->getExportedNode() !== null) { + $exportedNodes[] = $dependencies->getExportedNode(); + } + } catch (AnalysedCodeException) { + // pass + } catch (IdentifierNotFound) { // pass - } catch (IdentifierNotFound $e) { + } catch (UnableToCompileNode) { // pass } }; @@ -182,149 +223,163 @@ public function analyseFile( $this->nodeScopeResolver->processNodes( $parserNodes, $scope, - $nodeCallback + $nodeCallback, ); - $linesToIgnoreKeys = array_fill_keys($linesToIgnore, true); - $unmatchedLineIgnores = $linesToIgnoreKeys; - foreach ($temporaryFileErrors as $tmpFileError) { - $line = $tmpFileError->getLine(); - if ( - $line !== null - && $tmpFileError->canBeIgnored() - && array_key_exists($line, $linesToIgnoreKeys) - ) { - unset($unmatchedLineIgnores[$line]); - continue; - } - $fileErrors[] = $tmpFileError; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + $temporaryFileErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $fileError) { + $fileErrors[] = $fileError; } - - if ($this->reportUnmatchedIgnoredErrors) { - foreach (array_keys($unmatchedLineIgnores) as $line) { - $traitFilePath = null; - if ($scope->isInTrait()) { - $traitReflection = $scope->getTraitReflection(); - if ($traitReflection->getFileName() !== false) { - $traitFilePath = $traitReflection->getFileName(); - } - } - $fileErrors[] = new Error( - sprintf('No error to ignore is reported on line %d.', $line), - $scope->getFileDescription(), - $line, - false, - $scope->getFile(), - $traitFilePath, - 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, false); - } catch (\PHPStan\Parser\ParserErrorsException $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(), $file, $error->getStartLine() !== -1 ? $error->getStartLine() : null, false); + $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } - } catch (\PHPStan\AnalysedCodeException $e) { - $fileErrors[] = new Error($e->getMessage(), $file, null, false, null, null, $e->getTip()); + } catch (AnalysedCodeException $e) { + $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, false); + $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'); } - return new FileAnalyserResult($fileErrors, array_values(array_unique($fileDependencies))); - } + $this->restoreCollectErrorsHandler(); - /** - * @param Node $node - * @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, + ); } - private function findLineToIgnoreComment(Comment $comment): ?int + /** + * @param Node[] $nodes + * @return array|null> + */ + private function getLinesToIgnoreFromTokens(array $nodes): array { - $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 (!isset($nodes[0])) { + return []; } - if (strpos($text, '@phpstan-ignore-line') !== false) { - return $line; - } - - return null; + /** @var array|null> */ + return $nodes[0]->getAttribute('linesToIgnore', []); } /** - * @param \PhpParser\Node $node - * @param Scope $scope * @param array $analysedFiles - * @return string[] */ - private function resolveDependencies( - \PhpParser\Node $node, - Scope $scope, - array $analysedFiles - ): array + private function collectErrors(array $analysedFiles): void { - $dependencies = []; - - foreach ($this->dependencyResolver->resolveDependencies($node, $scope) as $dependencyReflection) { - $dependencyFile = $dependencyReflection->getFileName(); - if ($dependencyFile === false) { - continue; + $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; } - $dependencyFile = $this->fileHelper->normalizePath($dependencyFile); - if ($scope->getFile() === $dependencyFile) { - continue; + $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; } - if (!isset($analysedFiles[$dependencyFile])) { - continue; + if (!isset($analysedFiles[$errfile])) { + return true; } - $dependencies[$dependencyFile] = $dependencyFile; + $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, $errno === E_USER_DEPRECATED))->withIdentifier('phpstan.php'); + + return true; + }); + } + + 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 array_values($dependencies); + return 'Unknown PHP error'; } } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index e61d9ec377..2aba60730f 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -2,27 +2,43 @@ namespace PHPStan\Analyser; -class FileAnalyserResult -{ - - /** @var Error[] */ - private array $errors; +use PHPStan\Collectors\CollectedData; +use PHPStan\Dependency\RootExportedNode; - /** @var array */ - private array $dependencies; +/** + * @phpstan-type LinesToIgnore = array|null>> + * @phpstan-import-type CollectorData from CollectedData + */ +final class FileAnalyserResult +{ /** - * @param Error[] $errors - * @param array $dependencies + * @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(array $errors, array $dependencies) + 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, + ) { - $this->errors = $errors; - $this->dependencies = $dependencies; } /** - * @return Error[] + * @return list */ public function getErrors(): array { @@ -30,11 +46,67 @@ 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 { return $this->dependencies; } + /** + * @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/Ignore/IgnoredErrorHelperResult.php b/src/Analyser/Ignore/IgnoredErrorHelperResult.php new file mode 100644 index 0000000000..529e1ae4a4 --- /dev/null +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -0,0 +1,242 @@ + $errors + * @param array> $otherIgnoreErrors + * @param array>> $ignoreErrorsByFile + * @param (string|mixed[])[] $ignoreErrors + */ + public function __construct( + private FileHelper $fileHelper, + private array $errors, + private array $otherIgnoreErrors, + private array $ignoreErrorsByFile, + private array $ignoreErrors, + private bool $reportUnmatchedIgnoredErrors, + ) + { + } + + /** + * @return list + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * @param Error[] $errors + * @param string[] $analysedFiles + */ + public function process( + array $errors, + bool $onlyFiles, + array $analysedFiles, + bool $hasInternalErrors, + ): IgnoredErrorHelperProcessedResult + { + $unmatchedIgnoredErrors = $this->ignoreErrors; + $stringErrors = []; + + $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, null); + if ($shouldBeIgnored) { + unset($unmatchedIgnoredErrors[$i]); + } + } else { + if (isset($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; + $realCount++; + $unmatchedIgnoredErrors[$i]['realCount'] = $realCount; + + if (!isset($unmatchedIgnoredErrors[$i]['file'])) { + $unmatchedIgnoredErrors[$i]['file'] = $error->getFile(); + $unmatchedIgnoredErrors[$i]['line'] = $error->getLine(); + } + + if ($realCount > $ignore['count']) { + $shouldBeIgnored = false; + } + } else { + unset($unmatchedIgnoredErrors[$i]); + } + } + } elseif (isset($ignore['paths'])) { + foreach ($ignore['paths'] as $j => $ignorePath) { + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignorePath); + if (!$shouldBeIgnored) { + continue; + } + + if (isset($unmatchedIgnoredErrors[$i])) { + if (!is_array($unmatchedIgnoredErrors[$i])) { + throw new ShouldNotHappenException(); + } + unset($unmatchedIgnoredErrors[$i]['paths'][$j]); + if (isset($unmatchedIgnoredErrors[$i]['paths']) && count($unmatchedIgnoredErrors[$i]['paths']) === 0) { + unset($unmatchedIgnoredErrors[$i]); + } + } + break; + } + } else { + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, null); + if ($shouldBeIgnored) { + unset($unmatchedIgnoredErrors[$i]); + } + } + } + + if ($shouldBeIgnored) { + if (!$error->canBeIgnored()) { + $stringErrors[] = sprintf( + 'Error message "%s" cannot be ignored, use excludePaths instead.', + $error->getMessage(), + ); + return true; + } + return false; + } + + return true; + }; + + $ignoredErrors = []; + foreach ($errors as $errorIndex => $error) { + $filePath = $this->fileHelper->normalizePath($error->getFilePath()); + if (isset($this->ignoreErrorsByFile[$filePath])) { + foreach ($this->ignoreErrorsByFile[$filePath] as $ignoreError) { + $i = $ignoreError['index']; + $ignore = $ignoreError['ignoreError']; + $result = $processIgnoreError($error, $i, $ignore); + if (!$result) { + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; + } + } + } + + $traitFilePath = $error->getTraitFilePath(); + if ($traitFilePath !== null) { + $normalizedTraitFilePath = $this->fileHelper->normalizePath($traitFilePath); + if (isset($this->ignoreErrorsByFile[$normalizedTraitFilePath])) { + foreach ($this->ignoreErrorsByFile[$normalizedTraitFilePath] as $ignoreError) { + $i = $ignoreError['index']; + $ignore = $ignoreError['ignoreError']; + $result = $processIgnoreError($error, $i, $ignore); + if (!$result) { + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; + } + } + } + } + + foreach ($this->otherIgnoreErrors as $ignoreError) { + $i = $ignoreError['index']; + $ignore = $ignoreError['ignoreError']; + + $result = $processIgnoreError($error, $i, $ignore); + if (!$result) { + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; + } + } + } + + $errors = array_values($errors); + + foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { + if (!isset($unmatchedIgnoredError['count']) || !isset($unmatchedIgnoredError['realCount'])) { + continue; + } + + if ($unmatchedIgnoredError['realCount'] <= $unmatchedIgnoredError['count']) { + continue; + } + + $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))->withIdentifier('ignore.count'); + } + + $analysedFilesKeys = array_fill_keys($analysedFiles, true); + + 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( + '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))->withIdentifier('ignore.count'); + } + } elseif (isset($unmatchedIgnoredError['realPath'])) { + if (!array_key_exists($unmatchedIgnoredError['realPath'], $analysedFilesKeys)) { + continue; + } + + $errors[] = (new Error( + sprintf( + 'Ignored error pattern %s was not matched in reported errors.', + IgnoredError::stringifyPattern($unmatchedIgnoredError), + ), + $unmatchedIgnoredError['realPath'], + null, + false, + ))->withIdentifier('ignore.unmatched'); + } elseif (!$onlyFiles) { + $stringErrors[] = sprintf( + 'Ignored error pattern %s was not matched in reported errors.', + IgnoredError::stringifyPattern($unmatchedIgnoredError), + ); + } + } + } + + 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 f683a95ee1..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) { - $fileExcluder = new FileExcluder($fileHelper, [$path], []); - - if (\Nette\Utils\Strings::match($errorMessage, $ignoredErrorPattern) === null) { - return false; - } - - $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); - if (!$isExcluded && $error->getTraitFilePath() !== null) { - return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); - } - - return $isExcluded; - } - - return \Nette\Utils\Strings::match($errorMessage, $ignoredErrorPattern) !== null; - } - -} diff --git a/src/Analyser/IgnoredErrorHelper.php b/src/Analyser/IgnoredErrorHelper.php deleted file mode 100644 index 80219bfa74..0000000000 --- a/src/Analyser/IgnoredErrorHelper.php +++ /dev/null @@ -1,155 +0,0 @@ -ignoredRegexValidator = $ignoredRegexValidator; - $this->fileHelper = $fileHelper; - $this->ignoreErrors = $ignoreErrors; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; - } - - public function initialize(): IgnoredErrorHelperResult - { - $otherIgnoreErrors = []; - $ignoreErrorsByFile = []; - $warnings = []; - $errors = []; - foreach ($this->ignoreErrors as $i => $ignoreError) { - try { - 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, - ]; - } - - $ignoreMessage = $ignoreError['message']; - \Nette\Utils\Strings::match('', $ignoreMessage); - if (isset($ignoreError['count'])) { - continue; // ignoreError coming from baseline will be correct - } - $validationResult = $this->ignoredRegexValidator->validate($ignoreMessage); - $ignoredTypes = $validationResult->getIgnoredTypes(); - if (count($ignoredTypes) > 0) { - $warnings[] = $this->createIgnoredTypesWarning($ignoreMessage, $ignoredTypes); - } - - if ($validationResult->hasAnchorsInTheMiddle()) { - $warnings[] = $this->createAnchorInTheMiddleWarning($ignoreMessage); - } - - if ($validationResult->areAllErrorsIgnored()) { - $errors[] = sprintf("Ignored error %s has an unescaped '||' which leads to ignoring all errors. Use '\\|\\|' instead.", $ignoreMessage); - } - } else { - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; - $ignoreMessage = $ignoreError; - \Nette\Utils\Strings::match('', $ignoreMessage); - $validationResult = $this->ignoredRegexValidator->validate($ignoreMessage); - $ignoredTypes = $validationResult->getIgnoredTypes(); - if (count($ignoredTypes) > 0) { - $warnings[] = $this->createIgnoredTypesWarning($ignoreMessage, $ignoredTypes); - } - - if ($validationResult->hasAnchorsInTheMiddle()) { - $warnings[] = $this->createAnchorInTheMiddleWarning($ignoreMessage); - } - - if ($validationResult->areAllErrorsIgnored()) { - $errors[] = sprintf("Ignored error %s has an unescaped '||' which leads to ignoring all errors. Use '\\|\\|' instead.", $ignoreMessage); - } - } - } catch (\Nette\Utils\RegexpException $e) { - $errors[] = $e->getMessage(); - } - } - - return new IgnoredErrorHelperResult($this->fileHelper, $errors, $warnings, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors); - } - - /** - * @param string $regex - * @param array $ignoredTypes - * @return string - */ - private function createIgnoredTypesWarning(string $regex, array $ignoredTypes): string - { - return sprintf( - "Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s", - $regex, - sprintf( - "It ignores all errors containing the following types:\n%s", - implode("\n", array_map(static function (string $typeDescription): string { - return sprintf('* %s', $typeDescription); - }, array_keys($ignoredTypes))) - ) - ); - } - - private function createAnchorInTheMiddleWarning(string $regex): string - { - return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex); - } - -} diff --git a/src/Analyser/IgnoredErrorHelperResult.php b/src/Analyser/IgnoredErrorHelperResult.php deleted file mode 100644 index e3a5a49f14..0000000000 --- a/src/Analyser/IgnoredErrorHelperResult.php +++ /dev/null @@ -1,260 +0,0 @@ -> */ - private array $otherIgnoreErrors; - - /** @var array>> */ - private array $ignoreErrorsByFile; - - /** @var (string|mixed[])[] */ - private array $ignoreErrors; - - private bool $reportUnmatchedIgnoredErrors; - - /** - * @param FileHelper $fileHelper - * @param string[] $errors - * @param string[] $warnings - * @param array> $otherIgnoreErrors - * @param array>> $ignoreErrorsByFile - * @param (string|mixed[])[] $ignoreErrors - * @param bool $reportUnmatchedIgnoredErrors - */ - public function __construct( - FileHelper $fileHelper, - array $errors, - array $warnings, - array $otherIgnoreErrors, - array $ignoreErrorsByFile, - array $ignoreErrors, - bool $reportUnmatchedIgnoredErrors - ) - { - $this->fileHelper = $fileHelper; - $this->errors = $errors; - $this->warnings = $warnings; - $this->otherIgnoreErrors = $otherIgnoreErrors; - $this->ignoreErrorsByFile = $ignoreErrorsByFile; - $this->ignoreErrors = $ignoreErrors; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; - } - - /** - * @return string[] - */ - public function getErrors(): array - { - return $this->errors; - } - - /** - * @return string[] - */ - public function getWarnings(): array - { - return $this->warnings; - } - - /** - * @param Error[] $errors - * @param string[] $analysedFiles - * @return string[]|Error[] - */ - public function process( - array $errors, - bool $onlyFiles, - array $analysedFiles, - bool $hasInternalErrors - ): array - { - $unmatchedIgnoredErrors = $this->ignoreErrors; - $addErrors = []; - - $processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$addErrors): bool { - $shouldBeIgnored = false; - if (is_string($ignore)) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null); - if ($shouldBeIgnored) { - unset($unmatchedIgnoredErrors[$i]); - } - } else { - if (isset($ignore['path'])) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], $ignore['path']); - if ($shouldBeIgnored) { - if (isset($ignore['count'])) { - $realCount = $unmatchedIgnoredErrors[$i]['realCount'] ?? 0; - $realCount++; - $unmatchedIgnoredErrors[$i]['realCount'] = $realCount; - - if (!isset($unmatchedIgnoredErrors[$i]['file'])) { - $unmatchedIgnoredErrors[$i]['file'] = $error->getFile(); - $unmatchedIgnoredErrors[$i]['line'] = $error->getLine(); - } - - if ($realCount > $ignore['count']) { - $shouldBeIgnored = false; - } - } else { - unset($unmatchedIgnoredErrors[$i]); - } - } - } elseif (isset($ignore['paths'])) { - foreach ($ignore['paths'] as $j => $ignorePath) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], $ignorePath); - if (!$shouldBeIgnored) { - continue; - } - - if (isset($unmatchedIgnoredErrors[$i])) { - if (!is_array($unmatchedIgnoredErrors[$i])) { - throw new \PHPStan\ShouldNotHappenException(); - } - unset($unmatchedIgnoredErrors[$i]['paths'][$j]); - if (isset($unmatchedIgnoredErrors[$i]['paths']) && count($unmatchedIgnoredErrors[$i]['paths']) === 0) { - unset($unmatchedIgnoredErrors[$i]); - } - } - break; - } - } else { - throw new \PHPStan\ShouldNotHappenException(); - } - } - - if ($shouldBeIgnored) { - if (!$error->canBeIgnored()) { - $addErrors[] = sprintf( - 'Error message "%s" cannot be ignored, use excludes_analyse instead.', - $error->getMessage() - ); - return true; - } - return false; - } - - return true; - }; - - $errors = array_values(array_filter($errors, function (Error $error) use ($processIgnoreError): bool { - $filePath = $this->fileHelper->normalizePath($error->getFilePath()); - if (isset($this->ignoreErrorsByFile[$filePath])) { - foreach ($this->ignoreErrorsByFile[$filePath] as $ignoreError) { - $i = $ignoreError['index']; - $ignore = $ignoreError['ignoreError']; - $result = $processIgnoreError($error, $i, $ignore); - if (!$result) { - return false; - } - } - } - - $traitFilePath = $error->getTraitFilePath(); - if ($traitFilePath !== null) { - $normalizedTraitFilePath = $this->fileHelper->normalizePath($traitFilePath); - if (isset($this->ignoreErrorsByFile[$normalizedTraitFilePath])) { - foreach ($this->ignoreErrorsByFile[$normalizedTraitFilePath] as $ignoreError) { - $i = $ignoreError['index']; - $ignore = $ignoreError['ignoreError']; - $result = $processIgnoreError($error, $i, $ignore); - if (!$result) { - return false; - } - } - } - } - - foreach ($this->otherIgnoreErrors as $ignoreError) { - $i = $ignoreError['index']; - $ignore = $ignoreError['ignoreError']; - - $result = $processIgnoreError($error, $i, $ignore); - if (!$result) { - return false; - } - } - - return true; - })); - - foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { - if (!isset($unmatchedIgnoredError['count']) || !isset($unmatchedIgnoredError['realCount'])) { - continue; - } - - if ($unmatchedIgnoredError['realCount'] <= $unmatchedIgnoredError['count']) { - continue; - } - - $addErrors[] = 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); - } - - $errors = array_merge($errors, $addErrors); - - $analysedFilesKeys = array_fill_keys($analysedFiles, true); - - if ($this->reportUnmatchedIgnoredErrors && !$hasInternalErrors) { - foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { - if ( - isset($unmatchedIgnoredError['count']) - && isset($unmatchedIgnoredError['realCount']) - && (isset($unmatchedIgnoredError['realPath']) || !$onlyFiles) - ) { - if ($unmatchedIgnoredError['realCount'] < $unmatchedIgnoredError['count']) { - $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); - } - } elseif (isset($unmatchedIgnoredError['realPath'])) { - if (!array_key_exists($unmatchedIgnoredError['realPath'], $analysedFilesKeys)) { - continue; - } - - $errors[] = new Error( - sprintf( - 'Ignored error pattern %s was not matched in reported errors.', - IgnoredError::stringifyPattern($unmatchedIgnoredError) - ), - $unmatchedIgnoredError['realPath'], - null, - false - ); - } elseif (!$onlyFiles) { - $errors[] = sprintf( - 'Ignored error pattern %s was not matched in reported errors.', - IgnoredError::stringifyPattern($unmatchedIgnoredError) - ); - } - } - } - - return $errors; - } - -} 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 b2460fbed3..0000000000 --- a/src/Analyser/LazyScopeFactory.php +++ /dev/null @@ -1,102 +0,0 @@ -scopeClass = $scopeClass; - $this->container = $container; - $this->dynamicConstantNames = $container->getParameter('dynamicConstantNames'); - $this->treatPhpDocTypesAsCertain = $container->getParameter('treatPhpDocTypesAsCertain'); - } - - /** - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack - * - * @return MutatingScope - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [] - ): MutatingScope - { - $scopeClass = $this->scopeClass; - if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new \PHPStan\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->getByType(\PHPStan\Parser\Parser::class), - $context, - $declareStrictTypes, - $constantTypes, - $function, - $namespace, - $variablesTypes, - $moreSpecificTypes, - $inClosureBindScopeClass, - $anonymousFunctionReflection, - $inFirstLevelStatement, - $currentlyAssignedExpressions, - $nativeExpressionTypes, - $inFunctionCallsStack, - $this->dynamicConstantNames, - $this->treatPhpDocTypesAsCertain - ); - } - -} 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 455edb95fc..16a9c15f05 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2,9 +2,12 @@ namespace PHPStan\Analyser; -use Nette\Utils\Strings; +use ArrayAccess; +use Closure; +use Generator; 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; @@ -19,63 +22,108 @@ 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 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\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; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\TrivialParametersAcceptor; 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\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\CallableType; -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\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\IterableType; 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\StringType; use PHPStan\Type\ThisType; @@ -87,160 +135,121 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; - -class MutatingScope implements Scope +use stdClass; +use Throwable; +use function abs; +use function array_filter; +use function array_key_exists; +use function array_key_first; +use function array_keys; +use function array_map; +use function array_merge; +use function array_pop; +use function array_slice; +use function array_values; +use function count; +use function explode; +use function get_class; +use function implode; +use function in_array; +use function is_array; +use function is_bool; +use function is_numeric; +use function is_string; +use function ltrim; +use function md5; +use function sprintf; +use function str_starts_with; +use function strlen; +use function strtolower; +use function substr; +use function usort; +use const PHP_INT_MAX; +use const PHP_INT_MIN; + +final class MutatingScope implements Scope { - 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 \PHPStan\Analyser\ScopeFactory $scopeFactory; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Type\DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry; - - private OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry; - - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; + private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - private Parser $parser; + private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; - private \PHPStan\Analyser\ScopeContext $context; - - /** @var \PHPStan\Type\Type[] */ + /** @var Type[] */ private array $resolvedTypes = []; - private bool $declareStrictTypes; - - /** @var array */ - private array $constantTypes; + /** @var array */ + private array $truthyScopes = []; - /** @var \PHPStan\Reflection\FunctionReflection|MethodReflection|null */ - private $function; + /** @var array */ + private array $falseyScopes = []; + /** @var non-empty-string|null */ private ?string $namespace; - /** @var \PHPStan\Analyser\VariableTypeHolder[] */ - private array $variableTypes; - - /** @var \PHPStan\Analyser\VariableTypeHolder[] */ - private array $moreSpecificTypes; - - private ?string $inClosureBindScopeClass; - - private ?ParametersAcceptor $anonymousFunctionReflection; - - private bool $inFirstLevelStatement; - - /** @var array */ - private array $currentlyAssignedExpressions; - - /** @var array */ - private array $inFunctionCallsStack; + private ?self $scopeOutOfFirstLevelStatement = null; - /** @var array */ - private array $nativeExpressionTypes; + private ?self $scopeWithPromotedNativeTypes = null; - /** @var string[] */ - private array $dynamicConstantNames; - - private bool $treatPhpDocTypesAsCertain; + private static int $resolveClosureTypeDepth = 0; /** - * @param \PHPStan\Analyser\ScopeFactory $scopeFactory - * @param ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry - * @param \PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry - * @param \PhpParser\PrettyPrinter\Standard $printer - * @param \PHPStan\Analyser\TypeSpecifier $typeSpecifier - * @param \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder - * @param Parser $parser - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement + * @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 - * @paarm bool $treatPhpDocTypesAsCertain + * @param array $currentlyAllowedUndefinedExpressions + * @param array $nativeExpressionTypes + * @param list $inFunctionCallsStack */ public function __construct( - ScopeFactory $scopeFactory, - ReflectionProvider $reflectionProvider, - DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, - OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry, - \PhpParser\PrettyPrinter\Standard $printer, - TypeSpecifier $typeSpecifier, - PropertyReflectionFinder $propertyReflectionFinder, - Parser $parser, - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, + private InternalScopeFactory $scopeFactory, + private ReflectionProvider $reflectionProvider, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, + 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 PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - array $dynamicConstantNames = [], - bool $treatPhpDocTypesAsCertain = true + private array $expressionTypes = [], + private array $nativeExpressionTypes = [], + private array $conditionalExpressions = [], + private array $inClosureBindScopeClasses = [], + private ?ParametersAcceptor $anonymousFunctionReflection = null, + private bool $inFirstLevelStatement = true, + private array $currentlyAssignedExpressions = [], + private array $currentlyAllowedUndefinedExpressions = [], + private array $inFunctionCallsStack = [], + private bool $afterExtractCall = false, + private ?Scope $parentScope = null, + private bool $nativeTypesPromoted = false, ) { if ($namespace === '') { $namespace = null; } - $this->scopeFactory = $scopeFactory; - $this->reflectionProvider = $reflectionProvider; - $this->dynamicReturnTypeExtensionRegistry = $dynamicReturnTypeExtensionRegistry; - $this->operatorTypeSpecifyingExtensionRegistry = $operatorTypeSpecifyingExtensionRegistry; - $this->printer = $printer; - $this->typeSpecifier = $typeSpecifier; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->parser = $parser; - $this->context = $context; - $this->declareStrictTypes = $declareStrictTypes; - $this->constantTypes = $constantTypes; - $this->function = $function; $this->namespace = $namespace; - $this->variableTypes = $variablesTypes; - $this->moreSpecificTypes = $moreSpecificTypes; - $this->inClosureBindScopeClass = $inClosureBindScopeClass; - $this->anonymousFunctionReflection = $anonymousFunctionReflection; - $this->inFirstLevelStatement = $inFirstLevelStatement; - $this->currentlyAssignedExpressions = $currentlyAssignedExpressions; - $this->nativeExpressionTypes = $nativeExpressionTypes; - $this->inFunctionCallsStack = $inFunctionCallsStack; - $this->dynamicConstantNames = $dynamicConstantNames; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } + /** @api */ public function getFile(): string { return $this->context->getFile(); } + /** @api */ public function getFileDescription(): string { if ($this->context->getTraitReflection() === null) { @@ -250,23 +259,24 @@ public function getFileDescription(): string /** @var ClassReflection $classReflection */ $classReflection = $this->context->getClassReflection(); - $className = sprintf('class %s', $classReflection->getDisplayName()); - if ($classReflection->isAnonymous()) { - $className = 'anonymous class'; + $className = $classReflection->getDisplayName(); + if (!$classReflection->isAnonymous()) { + $className = sprintf('class %s', $className); } $traitReflection = $this->context->getTraitReflection(); - if ($traitReflection->getFileName() === false) { - throw new \PHPStan\ShouldNotHappenException(); + if ($traitReflection->getFileName() === null) { + throw new ShouldNotHappenException(); } return sprintf( '%s (in context of %s)', $traitReflection->getFileName(), - $className + $className, ); } + /** @api */ public function isDeclareStrictTypes(): bool { return $this->declareStrictTypes; @@ -276,143 +286,421 @@ public function enterDeclareStrictTypes(): self { return $this->scopeFactory->create( $this->context, - true + true, + null, + null, + $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, ); } + /** @api */ public function isInClass(): bool { return $this->context->getClassReflection() !== null; } + /** @api */ public function isInTrait(): bool { return $this->context->getTraitReflection() !== null; } + /** @api */ public function getClassReflection(): ?ClassReflection { return $this->context->getClassReflection(); } + /** @api */ public function getTraitReflection(): ?ClassReflection { return $this->context->getTraitReflection(); } /** - * @return \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null + * @api */ - public function getFunction() + public function getFunction(): ?PhpFunctionFromParserNodeReflection { return $this->function; } + /** @api */ public function getFunctionName(): ?string { return $this->function !== null ? $this->function->getName() : null; } + /** @api */ public function getNamespace(): ?string { return $this->namespace; } - /** - * @return array - */ - private function getVariableTypes(): array + /** @api */ + public function getParentScope(): ?Scope + { + return $this->parentScope; + } + + /** @api */ + public function canAnyVariableExist(): bool + { + return ($this->function === null && !$this->isInAnonymousFunction()) || $this->afterExtractCall; + } + + public function afterExtractCall(): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->expressionTypes, + $this->nativeExpressionTypes, + [], + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + true, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + public function afterClearstatcacheCall(): self + { + $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(). + foreach ([ + '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', + ] as $functionName) { + if (!str_starts_with($exprString, $functionName . '(') && !str_starts_with($exprString, '\\' . $functionName . '(')) { + continue; + } + + unset($expressionTypes[$exprString]); + continue 2; + } + } + 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, + ); + } + + public function afterOpenSslCall(string $openSslFunctionName): self { - return $this->variableTypes; + $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, + ); } + /** @api */ public function hasVariableType(string $variableName): TrinaryLogic { if ($this->isGlobalVariable($variableName)) { return TrinaryLogic::createYes(); } - if (!isset($this->variableTypes[$variableName])) { - if ($this->function === null && !$this->isInAnonymousFunction()) { + $varExprString = '$' . $variableName; + if (!isset($this->expressionTypes[$varExprString])) { + if ($this->canAnyVariableExist()) { return TrinaryLogic::createMaybe(); } 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 \PHPStan\Analyser\UndefinedVariableException($this, $variableName); + 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 list + */ + public function getDefinedVariables(): array + { + $variables = []; + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } + if (!$holder->getCertainty()->yes()) { + continue; + } + + $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; } 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 */ public function hasConstant(Name $name): bool { $isCompilerHaltOffset = $name->toString() === '__COMPILER_HALT_OFFSET__'; 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 { $nodes = $this->parser->parseFile($this->getFile()); foreach ($nodes as $node) { - if ($node instanceof \PhpParser\Node\Stmt\HaltCompiler) { + if ($node instanceof Node\Stmt\HaltCompiler) { return true; } } @@ -420,17 +708,20 @@ private function fileHasCompilerHaltStatementCalls(): bool return false; } + /** @api */ public function isInAnonymousFunction(): bool { return $this->anonymousFunctionReflection !== null; } + /** @api */ public function getAnonymousFunctionReflection(): ?ParametersAcceptor { return $this->anonymousFunctionReflection; } - public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type + /** @api */ + public function getAnonymousFunctionReturnType(): ?Type { if ($this->anonymousFunctionReflection === null) { return null; @@ -439,129 +730,185 @@ public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type return $this->anonymousFunctionReflection->getReturnType(); } + /** @api */ public function getType(Expr $node): Type { + if ($node instanceof GetIterableKeyTypeExpr) { + return $this->getIterableKeyType($this->getType($node->getExpr())); + } + if ($node instanceof GetIterableValueTypeExpr) { + 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) { + return new ErrorType(); + } + + return $propertyReflection->getReadableType(); + } + $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 { - if ($node instanceof Expr\Exit_) { - return new NeverType(); + $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())); } - if ( - $node instanceof Expr\BinaryOp\Greater - || $node instanceof Expr\BinaryOp\GreaterOrEqual - || $node instanceof Expr\BinaryOp\Smaller - || $node instanceof Expr\BinaryOp\SmallerOrEqual - ) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); + $parts[] = sprintf(':%d', count($this->inFunctionCallsStack)); + foreach ($this->inFunctionCallsStack as [$method, $parameter]) { + if ($parameter === null) { + $parts[] = ',null'; + continue; + } - if ($rightType instanceof ConstantIntegerType) { - $rightValue = $rightType->getValue(); + $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache())); + } - if ($node instanceof Expr\BinaryOp\Greater) { - $rightIntervalType = IntegerRangeType::fromInterval($rightValue + 1, null); - } elseif ($node instanceof Expr\BinaryOp\GreaterOrEqual) { - $rightIntervalType = IntegerRangeType::fromInterval($rightValue, null); - } elseif ($node instanceof Expr\BinaryOp\Smaller) { - $rightIntervalType = IntegerRangeType::fromInterval(null, $rightValue - 1); - } else { - $rightIntervalType = IntegerRangeType::fromInterval(null, $rightValue); - } + 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; + } + } - return $rightIntervalType->isSuperTypeOf($leftType->toInteger())->toBooleanType(); - } elseif ($leftType instanceof ConstantIntegerType) { - $leftValue = $leftType->getValue(); + if ($node instanceof Expr\Exit_ || $node instanceof Expr\Throw_) { + return new NonAcceptingNeverType(); + } - if ($node instanceof Expr\BinaryOp\Smaller) { - $leftIntervalType = IntegerRangeType::fromInterval($leftValue + 1, null); - } elseif ($node instanceof Expr\BinaryOp\SmallerOrEqual) { - $leftIntervalType = IntegerRangeType::fromInterval($leftValue, null); - } elseif ($node instanceof Expr\BinaryOp\Greater) { - $leftIntervalType = IntegerRangeType::fromInterval(null, $leftValue - 1); - } else { - $leftIntervalType = IntegerRangeType::fromInterval(null, $leftValue); - } + if (!$node instanceof Variable && $this->hasExpressionType($node)->yes()) { + return $this->expressionTypes[$exprString]->getType(); + } - return $leftIntervalType->isSuperTypeOf($rightType->toInteger())->toBooleanType(); - } + if ($node instanceof AlwaysRememberedExpr) { + return $node->getExprType(); + } - return new BooleanType(); + if ($node instanceof Expr\BinaryOp\Smaller) { + return $this->getType($node->left)->isSmallerThan($this->getType($node->right), $this->phpVersion)->toBooleanType(); } - if ( - $node instanceof Expr\BinaryOp\Equal - || $node instanceof Expr\BinaryOp\NotEqual - || $node instanceof Expr\Empty_ - ) { - return new BooleanType(); + if ($node instanceof Expr\BinaryOp\SmallerOrEqual) { + return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right), $this->phpVersion)->toBooleanType(); } - if ($node instanceof Expr\Isset_) { - $result = new ConstantBooleanType(true); - foreach ($node->vars as $var) { - if ($var instanceof Expr\ArrayDimFetch && $var->dim !== null) { - $hasOffset = $this->getType($var->var)->hasOffsetValueType( - $this->getType($var->dim) - )->toBooleanType(); - if ($hasOffset instanceof ConstantBooleanType) { - if (!$hasOffset->getValue()) { - return $hasOffset; - } + if ($node instanceof Expr\BinaryOp\Greater) { + return $this->getType($node->right)->isSmallerThan($this->getType($node->left), $this->phpVersion)->toBooleanType(); + } - continue; - } + if ($node instanceof Expr\BinaryOp\GreaterOrEqual) { + return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left), $this->phpVersion)->toBooleanType(); + } - $result = $hasOffset; - continue; - } + 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 ($var instanceof Expr\Variable && is_string($var->name)) { - $variableType = $this->resolveType($var); - $isNullSuperType = (new NullType())->isSuperTypeOf($variableType); - $has = $this->hasVariableType($var->name); - if ($has->no() || $isNullSuperType->yes()) { - return new ConstantBooleanType(false); - } + $leftType = $this->getType($node->left); + $rightType = $this->getType($node->right); - if ($has->maybe() || !$isNullSuperType->no()) { - $result = new BooleanType(); - } - continue; + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; + } + + 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 = $type->isNull(); + $isFalsey = $type->toBoolean()->isFalse(); + if ($isNull->maybe()) { + return null; + } + if ($isFalsey->maybe()) { + return null; + } + + if ($isNull->yes()) { + return $isFalsey->no(); } + return !$isFalsey->yes(); + }); + if ($result === null) { return new BooleanType(); } - return $result; + return new ConstantBooleanType(!$result); } - if ($node instanceof \PhpParser\Node\Expr\BooleanNot) { - if ($this->treatPhpDocTypesAsCertain) { - $exprBooleanType = $this->getType($node->expr)->toBoolean(); - } else { - $exprBooleanType = $this->getNativeType($node->expr)->toBoolean(); - } + if ($node instanceof Node\Expr\BooleanNot) { + $exprBooleanType = $this->getType($node->expr)->toBoolean(); if ($exprBooleanType instanceof ConstantBooleanType) { return new ConstantBooleanType(!$exprBooleanType->getValue()); } @@ -569,41 +916,35 @@ private function resolveType(Expr $node): Type return new BooleanType(); } + if ($node instanceof Node\Expr\BitwiseNot) { + return $this->initializerExprTypeResolver->getBitwiseNotType($node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } + if ( - $node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanAnd - || $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalAnd + $node instanceof Node\Expr\BinaryOp\BooleanAnd + || $node instanceof Node\Expr\BinaryOp\LogicalAnd ) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - } 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); } @@ -612,39 +953,30 @@ private function resolveType(Expr $node): Type } if ( - $node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanOr - || $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalOr + $node instanceof Node\Expr\BinaryOp\BooleanOr + || $node instanceof Node\Expr\BinaryOp\LogicalOr ) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - } 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); } @@ -652,21 +984,16 @@ private function resolveType(Expr $node): Type return new BooleanType(); } - if ($node instanceof \PhpParser\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(); - } + if ($node instanceof Node\Expr\BinaryOp\LogicalXor) { + $leftBooleanType = $this->getType($node->left)->toBoolean(); + $rightBooleanType = $this->getType($node->right)->toBoolean(); if ( $leftBooleanType instanceof ConstantBooleanType && $rightBooleanType instanceof ConstantBooleanType ) { return new ConstantBooleanType( - $leftBooleanType->getValue() xor $rightBooleanType->getValue() + $leftBooleanType->getValue() xor $rightBooleanType->getValue(), ); } @@ -674,93 +1001,15 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Identical) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType instanceof NullType - && !$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(); + return $this->richerScopeGetTypeHelper->getIdenticalResult($this, $node)->type; } if ($node instanceof Expr\BinaryOp\NotIdentical) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType instanceof NullType - && !$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(); + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($this, $node)->type; } if ($node instanceof Expr\Instanceof_) { - if ($this->treatPhpDocTypesAsCertain) { - $expressionType = $this->getType($node->expr); - } else { - $expressionType = $this->getNativeType($node->expr); - } + $expressionType = $this->getType($node->expr); if ( $this->isInTrait() && TypeUtils::findThisType($expressionType) !== null @@ -774,15 +1023,23 @@ private function resolveType(Expr $node): Type $uncertainty = false; if ($node->class instanceof Node\Name) { - $className = $this->resolveName($node->class); - $classType = new ObjectType($className); + $unresolvedClassName = $node->class->toString(); + if ( + strtolower($unresolvedClassName) === 'static' + && $this->isInClass() + ) { + $classType = new StaticType($this->getClassReflection()); + } else { + $className = $this->resolveName($node->class); + $classType = new ObjectType($className); + } } else { $classType = $this->getType($node->class); $classType = 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; } @@ -823,386 +1080,205 @@ 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); - } - - 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 && $rightStringType instanceof ConstantStringType) { - return $leftStringType->append($rightStringType); - } + if ($node instanceof Expr\BinaryOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - return new StringType(); + 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; - } + if ($node instanceof BinaryOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - $rightTypes = TypeUtils::getConstantScalars($this->getType($right)->toNumber()); - foreach ($rightTypes as $rightType) { - if ( - $rightType->getValue() === 0 - || $rightType->getValue() === 0.0 - ) { - return new ErrorType(); - } - } + 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 - || $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; - } + if ($node instanceof BinaryOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - $leftTypes = TypeUtils::getConstantScalars($this->getType($left)); - $rightTypes = TypeUtils::getConstantScalars($this->getType($right)); + if ($node instanceof Expr\AssignOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if (count($leftTypes) > 0 && count($rightTypes) > 0) { - $resultTypes = []; - foreach ($leftTypes as $leftType) { - foreach ($rightTypes as $rightType) { - $resultTypes[] = $this->calculateFromScalars($node, $leftType, $rightType); - } - } - return TypeCombinator::union(...$resultTypes); - } + if ($node instanceof BinaryOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Expr\AssignOp\Mod) { - 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 new IntegerType(); + return $this->initializerExprTypeResolver->getSpaceshipType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\AssignOp\Coalesce) { - return $this->getType(new BinaryOp\Coalesce($node->var, $node->expr, $node->getAttributes())); + if ($node instanceof BinaryOp\Div) { + return $this->initializerExprTypeResolver->getDivType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); } - if ($node instanceof Expr\BinaryOp\Coalesce) { - if ($node->left instanceof Expr\ArrayDimFetch && $node->left->dim !== null) { - $dimType = $this->getType($node->left->dim); - $varType = $this->getType($node->left->var); - $hasOffset = $varType->hasOffsetValueType($dimType); - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - if ($hasOffset->no()) { - return $rightType; - } elseif ($hasOffset->yes()) { - $offsetValueType = $varType->getOffsetValueType($dimType); - if ($offsetValueType->isSuperTypeOf(new NullType())->no()) { - return TypeCombinator::removeNull($leftType); - } - } - - return TypeCombinator::union( - TypeCombinator::removeNull($leftType), - $rightType - ); - } - - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - if ($leftType instanceof ErrorType || $leftType instanceof NullType) { - return $rightType; - } - - if ( - TypeCombinator::containsNull($leftType) - || $node->left instanceof PropertyFetch - || ( - $node->left instanceof Variable - && is_string($node->left->name) - && !$this->hasVariableType($node->left->name)->yes() - ) - ) { - return TypeCombinator::union( - TypeCombinator::removeNull($leftType), - $rightType - ); - } - - return TypeCombinator::removeNull($leftType); + 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\Clone_) { - return $this->getType($node->expr); + if ($node instanceof BinaryOp\Mod) { + return $this->initializerExprTypeResolver->getModType($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(); - } + if ($node instanceof Expr\AssignOp\Mod) { + return $this->initializerExprTypeResolver->getModType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - return new IntegerType(); + if ($node instanceof BinaryOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($node->left, $node->right, 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; - } + if ($node instanceof Expr\AssignOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - $leftType = $this->getType($left); - $rightType = $this->getType($right); - $stringType = new StringType(); + if ($node instanceof BinaryOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($stringType->isSuperTypeOf($leftType)->yes() && $stringType->isSuperTypeOf($rightType)->yes()) { - return $stringType; - } + if ($node instanceof Expr\AssignOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - if (TypeCombinator::union($leftType->toNumber(), $rightType->toNumber()) instanceof ErrorType) { - return new ErrorType(); - } + if ($node instanceof BinaryOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - return new IntegerType(); + if ($node instanceof Expr\AssignOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($node->var, $node->expr, 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; - } + if ($node instanceof BinaryOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - $leftType = $this->getType($left); - $rightType = $this->getType($right); + if ($node instanceof Expr\AssignOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - $operatorSigil = null; + if ($node instanceof BinaryOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr)); + } - if ($node instanceof BinaryOp) { - $operatorSigil = $node->getOperatorSigil(); - } + if ($node instanceof Expr\AssignOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($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\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($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\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr)); + } - /** @var Type[] $extensionTypes */ - $extensionTypes = []; + if ($node instanceof Expr\Clone_) { + return $this->getType($node->expr); + } - foreach ($operatorTypeSpecifyingExtensions as $extension) { - $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); + if ($node instanceof Node\Scalar\Int_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof String_) { + return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); + } elseif ($node instanceof Node\Scalar\InterpolatedString) { + $resultType = null; + foreach ($node->parts as $part) { + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $partType = $this->getType($part)->toString(); } - - if (count($extensionTypes) > 0) { - return TypeCombinator::union(...$extensionTypes); + if ($resultType === null) { + $resultType = $partType; + continue; } - } - if ($node instanceof Expr\AssignOp\Plus || $node instanceof Expr\BinaryOp\Plus) { - $leftConstantArrays = TypeUtils::getConstantArrays($leftType); - $rightConstantArrays = TypeUtils::getConstantArrays($rightType); + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + } - 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 $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( + $function, + $function->getVariants(), + ); } - return TypeCombinator::union(...$resultTypes); + return new ObjectType(Closure::class); } - $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); - } - return new ArrayType( - $keyType, - TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()) - ); + $callableType = $this->getType($node->name); + if (!$callableType->isCallable()->yes()) { + return new ObjectType(Closure::class); } - if ($leftType instanceof MixedType && $rightType instanceof MixedType) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - new ArrayType(new MixedType(), new MixedType()), - ]); - } + return $this->createFirstClassCallable( + null, + $callableType->getCallableParametersAcceptors($this), + ); } - $types = TypeCombinator::union($leftType, $rightType); - if ( - $leftType instanceof ArrayType - || $rightType instanceof ArrayType - || $types instanceof ArrayType - ) { - return new ErrorType(); - } + if ($node instanceof MethodCall) { + if (!$node->name instanceof Node\Identifier) { + return new ObjectType(Closure::class); + } - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); + $varType = $this->getType($node->var); + $method = $this->getMethodReflection($varType, $node->name->toString()); + if ($method === null) { + return new ObjectType(Closure::class); + } - if ( - (new FloatType())->isSuperTypeOf($leftNumberType)->yes() - || (new FloatType())->isSuperTypeOf($rightNumberType)->yes() - ) { - return new FloatType(); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } - if ($node instanceof Expr\AssignOp\Pow || $node instanceof Expr\BinaryOp\Pow) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); - } + if ($node instanceof Expr\StaticCall) { + if (!$node->class instanceof Name) { + return new ObjectType(Closure::class); + } - $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->name instanceof Node\Identifier) { + return new ObjectType(Closure::class); } - return new UnionType([new IntegerType(), new FloatType()]); - } + $classType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); + $methodName = $node->name->toString(); + if (!$classType->hasMethod($methodName)->yes()) { + return new ObjectType(Closure::class); + } - if ($types instanceof MixedType - || $leftType instanceof BenevolentUnionType - || $rightType instanceof BenevolentUnionType - ) { - return TypeUtils::toBenevolentUnion($resultType); + $method = $classType->getMethod($methodName, $this); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } - return $resultType; - } - - if ($node instanceof LNumber) { - return new ConstantIntegerType($node->value); - } elseif ($node instanceof String_) { - return new ConstantStringType($node->value); - } elseif ($node instanceof Node\Scalar\Encapsed) { - $constantString = new ConstantStringType(''); - foreach ($node->parts as $part) { - if ($part instanceof EncapsedStringPart) { - $partStringType = new ConstantStringType($part->value); - } else { - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); - } - if (!$partStringType instanceof ConstantStringType) { - return new StringType(); - } - } - - $constantString = $constantString->append($partStringType); + if ($node instanceof New_) { + return new ErrorType(); } - return $constantString; - } elseif ($node instanceof DNumber) { - return new ConstantFloatType($node->value); + + throw new ShouldNotHappenException(); } elseif ($node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction) { $parameters = []; $isVariadic = false; @@ -1224,148 +1300,414 @@ private function resolveType(Expr $node): Type $isVariadic = true; } if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $parameters[] = new NativeParameterReflection( $param->var->name, $firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex, - $this->getFunctionType($param->type, $param->type === null, false), + $this->getFunctionType($param->type, $this->isParameterValueNullable($param), false), $param->byRef ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $param->variadic, - $param->default !== null ? $this->getType($param->default) : null + $param->default !== null ? $this->getType($param->default) : null, ); } - if ($node->returnType === null && $node instanceof Expr\ArrowFunction) { - $returnType = $this->getType($node->expr); - } else { - $returnType = $this->getFunctionType($node->returnType, $node->returnType === null, false); - } - - return new ClosureType( - $parameters, - $returnType, - $isVariadic - ); - } elseif ($node instanceof New_) { - if ($node->class instanceof Name) { - $type = $this->exactInstantiation($node, $node->class->toString()); - if ($type !== null) { - return $type; + $callableParameters = 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); } - - $lowercasedClassName = strtolower($node->class->toString()); - if ($lowercasedClassName === 'static') { - if (!$this->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + } 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()); } - - return new StaticType($this->getClassReflection()->getName()); - } - if ($lowercasedClassName === 'parent') { - return new NonexistentParentClassType(); } - - return new ObjectType($node->class->toString()); } - if ($node->class instanceof Node\Stmt\Class_) { - $anonymousClassReflection = $this->reflectionProvider->getAnonymousClassReflection($node->class, $this); - return new ObjectType($anonymousClassReflection->getName()); - } + if ($node instanceof Expr\ArrowFunction) { + $arrowScope = $this->enterArrowFunctionWithoutReflection($node, $callableParameters); - $exprType = $this->getType($node->class); - return $this->getTypeToInstantiateForNew($exprType); + if ($node->expr instanceof Expr\Yield_ || $node->expr instanceof Expr\YieldFrom) { + $yieldNode = $node->expr; - } elseif ($node instanceof Array_) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - if (count($node->items) > 256) { - $arrayBuilder->degradeToGeneralArray(); - } - foreach ($node->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } + if ($yieldNode instanceof Expr\Yield_) { + if ($yieldNode->key === null) { + $keyType = new IntegerType(); + } else { + $keyType = $arrowScope->getType($yieldNode->key); + } - $valueType = $this->getType($arrayItem->value); - if ($arrayItem->unpack) { - if ($valueType instanceof ConstantArrayType) { - foreach ($valueType->getValueTypes() as $innerValueType) { - $arrayBuilder->setOffsetValueType(null, $innerValueType); + if ($yieldNode->value === null) { + $valueType = new NullType(); + } else { + $valueType = $arrowScope->getType($yieldNode->value); } } else { - $arrayBuilder->degradeToGeneralArray(); - $arrayBuilder->setOffsetValueType(new IntegerType(), $valueType->getIterableValueType()); + $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 { - $arrayBuilder->setOffsetValueType( - $arrayItem->key !== null ? $this->getType($arrayItem->key) : null, - $valueType - ); + $returnType = $arrowScope->getKeepVoidType($node->expr); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); + } } - } - return $arrayBuilder->getArray(); - } elseif ($node instanceof Int_) { - return $this->getType($node->expr)->toInteger(); - } elseif ($node instanceof Bool_) { - return $this->getType($node->expr)->toBoolean(); - } elseif ($node instanceof Double) { - return $this->getType($node->expr)->toFloat(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) { - return $this->getType($node->expr)->toString(); - } elseif ($node instanceof \PhpParser\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(''); - } + $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; + } - 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}'); - } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } - $function = $this->getFunction(); - if ($function === null) { - return new ConstantStringType(''); - } - if ($function instanceof MethodReflection) { - return new ConstantStringType( - sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName()) - ); - } + 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 = []; + $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 ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + + break; + } + + if (count($node->getStatementResult()->getExitPoints()) === 0) { + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + } + } else { + $onlyNeverExecutionEnds = false; + } + + return; + } + + if ($node instanceof Node\Stmt\Return_) { + $closureReturnStatements[] = [$node, $scope]; + } + + if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) { + return; + } + + $closureYieldStatements[] = [$node, $scope]; + }, StatementContext::createTopLevel()); + } finally { + self::$resolveClosureTypeDepth--; + } + + $throwPoints = $closureStatementResult->getThrowPoints(); + $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints()); + + $returnTypes = []; + $hasNull = false; + foreach ($closureReturnStatements as [$returnNode, $returnScope]) { + if ($returnNode->expr === null) { + $hasNull = true; + continue; + } + + $returnTypes[] = $returnScope->getType($returnNode->expr); + } + + if (count($returnTypes) === 0) { + if ($onlyNeverExecutionEnds === true && !$hasNull) { + $returnType = new NonAcceptingNeverType(); + } else { + $returnType = new VoidType(); + } + } else { + if ($onlyNeverExecutionEnds === true) { + $returnTypes[] = new NonAcceptingNeverType(); + } + if ($hasNull) { + $returnTypes[] = new NullType(); + } + $returnType = TypeCombinator::union(...$returnTypes); + } + + if (count($closureYieldStatements) > 0) { + $keyTypes = []; + $valueTypes = []; + foreach ($closureYieldStatements as [$yieldNode, $yieldScope]) { + if ($yieldNode instanceof Expr\Yield_) { + if ($yieldNode->key === null) { + $keyTypes[] = new IntegerType(); + } else { + $keyTypes[] = $yieldScope->getType($yieldNode->key); + } + + if ($yieldNode->value === null) { + $valueTypes[] = new NullType(); + } else { + $valueTypes[] = $yieldScope->getType($yieldNode->value); + } + + continue; + } + + $yieldFromType = $yieldScope->getType($yieldNode->expr); + $keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType); + $valueTypes[] = $yieldScope->getIterableValueType($yieldFromType); + } - return new ConstantStringType($function->getName()); - } elseif ($node instanceof Node\Scalar\MagicConst\Function_) { - if ($this->isInAnonymousFunction()) { - return new ConstantStringType('{closure}'); + $returnType = new GenericObjectType(Generator::class, [ + TypeCombinator::union(...$keyTypes), + TypeCombinator::union(...$valueTypes), + new MixedType(), + $returnType, + ]); + } else { + 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; + } } - $function = $this->getFunction(); - if ($function === null) { - return new ConstantStringType(''); + + foreach ($parameters as $parameter) { + if ($parameter->passedByReference()->no()) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref parameter', + true, + ); } - return new ConstantStringType($function->getName()); - } elseif ($node instanceof Node\Scalar\MagicConst\Trait_) { - if (!$this->isInTrait()) { - return new ConstantStringType(''); + $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) { + $type = $this->exactInstantiation($node, $node->class->toString()); + if ($type !== null) { + return $type; + } + + $lowercasedClassName = strtolower($node->class->toString()); + if ($lowercasedClassName === 'static') { + if (!$this->isInClass()) { + return new ErrorType(); + } + + return new StaticType($this->getClassReflection()); + } + if ($lowercasedClassName === 'parent') { + return new NonexistentParentClassType(); + } + + return new ObjectType($node->class->toString()); } - return new ConstantStringType($this->getTraitReflection()->getName(), true); + if ($node->class instanceof Node\Stmt\Class_) { + $anonymousClassReflection = $this->reflectionProvider->getAnonymousClassReflection($node->class, $this); + + return new ObjectType($anonymousClassReflection->getName()); + } + + $exprType = $this->getType($node->class); + return $exprType->getObjectTypeOrClassStringObjectType(); + + } elseif ($node instanceof Array_) { + 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_) { + return $this->getType($node->expr)->toBoolean(); + } elseif ($node instanceof Double) { + return $this->getType($node->expr)->toFloat(); + } elseif ($node instanceof Node\Expr\Cast\String_) { + 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) { + 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; } @@ -1384,557 +1726,1016 @@ 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); + $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 ($varType instanceof IntegerRangeType) { - $shift = $node instanceof Expr\PreInc ? +1 : -1; - return IntegerRangeType::fromInterval( - $varType->getMin() === PHP_INT_MIN ? PHP_INT_MIN : $varType->getMin() + $shift, - $varType->getMax() === PHP_INT_MAX ? PHP_INT_MAX : $varType->getMax() + $shift - ); + } elseif ($varType->isString()->yes()) { + if ($varType->isLiteralString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); + } + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); } - $stringType = new StringType(); - if ($stringType->isSuperTypeOf($varType)->yes()) { - return $stringType; + if ($node instanceof Expr\PreInc) { + return $this->getType(new BinaryOp\Plus($node->var, new Node\Scalar\Int_(1))); } - return $varType->toNumber(); + 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) { + $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { return new MixedType(); } - $generatorReturnType = GenericTypeVariableResolver::getType($yieldFromType, \Generator::class, 'TReturn'); - if ($generatorReturnType === null) { - return new MixedType(); + return $generatorReturnType; + } elseif ($node instanceof Expr\Match_) { + $cond = $node->cond; + $condType = $this->getType($cond); + $types = []; + + $matchScope = $this; + $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); + } } - return $generatorReturnType; + 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; + } + + if (count($arm->conds) === 0) { + throw new ShouldNotHappenException(); + } + + 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 FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); + } + + $filteringExprType = $matchScope->getType($filteringExpr); + + if (!$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); + } + + 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) { + return $this->getType(new BinaryOp\Coalesce($node->var, $node->expr, $node->getAttributes())); + } + + if ($node instanceof Expr\BinaryOp\Coalesce) { + $issetLeftExpr = new Expr\Isset_([$node->left]); + $leftType = $this->filterByTruthyValue($issetLeftExpr)->getType($node->left); + + $result = $this->issetCheck($node->left, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + 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), + $rightType, + ); + } + + return $rightType; } if ($node instanceof ConstFetch) { $constName = (string) $node->name; $loweredConstName = strtolower($constName); if ($loweredConstName === 'true') { - return new \PHPStan\Type\Constant\ConstantBooleanType(true); + return new ConstantBooleanType(true); } elseif ($loweredConstName === 'false') { - return new \PHPStan\Type\Constant\ConstantBooleanType(false); + return new ConstantBooleanType(false); } elseif ($loweredConstName === 'null') { return new NullType(); } - 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()); + + 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(), + ); + } + } + + $constantType = $this->constantResolver->resolveConstant($node->name, $this); + if ($constantType !== null) { + return $constantType; + } + + 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 ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); + } + + 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 ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $constName]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + if ($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(); } + + return $this->getVariableType($node->name); } - $constantName = new FullyQualified($constName); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + $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()); + } + + return TypeCombinator::union(...$types); } + } - if ($this->reflectionProvider->hasConstant($node->name, $this)) { - /** @var string $resolvedConstantName */ - $resolvedConstantName = $this->reflectionProvider->resolveConstantName($node->name, $this); - if ($resolvedConstantName === 'DIRECTORY_SEPARATOR') { - return new UnionType([ - new ConstantStringType('/'), - new ConstantStringType('\\'), - ]); + if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { + return $this->getNullsafeShortCircuitingType( + $node->var, + $this->getTypeFromArrayDimFetch( + $node, + $this->getType($node->dim), + $this->getType($node->var), + ), + ); + } + + 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); } - if ($resolvedConstantName === 'PATH_SEPARATOR') { - return new UnionType([ - new ConstantStringType(':'), - new ConstantStringType(';'), - ]); + + $returnType = $this->methodCallReturnType( + $this->getType($node->var), + $node->name->name, + $node, + ); + if ($returnType === null) { + $returnType = new ErrorType(); } - if ($resolvedConstantName === 'PHP_EOL') { - return new UnionType([ - new ConstantStringType("\n"), - new ConstantStringType("\r\n"), - ]); + 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 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)); + } + + return TypeCombinator::union( + $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null')))) + ->getType(new MethodCall($node->var, $node->name, $node->args)), + new NullType(), + ); + } + + 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 ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); + } + + return $callType; + } + + if ($node->class instanceof Name) { + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name); + } else { + $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } - if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { - return new IntegerType(); + + $callType = $this->methodCallReturnType( + $staticMethodCalledOnType, + $node->name->toString(), + $node, + ); + if ($callType === null) { + $callType = new ErrorType(); } - $constantType = $this->reflectionProvider->getConstant($node->name, $this)->getValueType(); + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); + } - return $this->resolveConstantType($resolvedConstantName, $constantType); + return $callType; } - return new ErrorType(); - } elseif ($node instanceof Node\Expr\ClassConstFetch && $node->name instanceof Node\Identifier) { - $constantName = $node->name->name; - 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()->getName())); - } + $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) { + 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) { + $returnType = new ErrorType(); } - if (in_array(strtolower($constantClass), $namesToResolve, true)) { - $resolvedName = $this->resolveName($node->class); - if ($resolvedName === 'parent' && strtolower($constantName) === 'class') { - return new ClassStringType(); + + 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)); + } + + return TypeCombinator::union( + $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null')))) + ->getType(new PropertyFetch($node->var, $node->name)), + new NullType(), + ); + } + + 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); } - $constantClassType = new ObjectType($resolvedName); + + return $nativeType; } - } else { - $constantClassType = $this->getType($node->class); + + if ($node->class instanceof Name) { + $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); + } else { + $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); + } + + $fetchType = $this->propertyFetchType( + $staticPropertyFetchedOnType, + $node->name->toString(), + $node, + ); + if ($fetchType === null) { + $fetchType = new ErrorType(); + } + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $fetchType); + } + + return $fetchType; } - if (strtolower($constantName) === 'class' && $constantClassType instanceof TypeWithClassName) { - return new ConstantStringType($constantClassType->getClassName(), true); + $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()), + ); } + } - $referencedClasses = TypeUtils::getDirectClassNames($constantClassType); - $types = []; - foreach ($referencedClasses as $referencedClass) { - if (!$this->reflectionProvider->hasClass($referencedClass)) { - continue; + if ($node instanceof FuncCall) { + if ($node->name instanceof Expr) { + $calledOnType = $this->getType($node->name); + if ($calledOnType->isCallable()->no()) { + return new ErrorType(); } - $propertyClassReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$propertyClassReflection->hasConstant($constantName)) { - continue; + return ParametersAcceptorSelector::selectFromArgs( + $this, + $node->getArgs(), + $calledOnType->getCallableParametersAcceptors($this), + null, + )->getReturnType(); + } + + if (!$this->reflectionProvider->hasFunction($node->name, $this)) { + return new ErrorType(); + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $this); + if ($this->nativeTypesPromoted) { + return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType(); + } + + if ($functionReflection->getName() === 'call_user_func') { + $result = ArgumentsNormalizer::reorderCallUserFuncArguments($node, $this); + if ($result !== null) { + [, $innerFuncCall] = $result; + + return $this->getType($innerFuncCall); } + } - $constantType = $propertyClassReflection->getConstant($constantName)->getValueType(); - if ( - $constantType instanceof ConstantType - && in_array(sprintf('%s::%s', $propertyClassReflection->getName(), $constantName), $this->dynamicConstantNames, true) - ) { - $constantType = $constantType->generalize(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $this, + $node->getArgs(), + $functionReflection->getVariants(), + $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; + } } - $types[] = $constantType; } - if (count($types) > 0) { - return TypeCombinator::union(...$types); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node); + } + + return new MixedType(); + } + + private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type + { + if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { + $varType = $this->getType($expr->var); + if (TypeCombinator::containsNull($varType)) { + return TypeCombinator::addNull($type); + } + + return $type; + } + + if ($expr instanceof Expr\ArrayDimFetch) { + return $this->getNullsafeShortCircuitingType($expr->var, $type); + } + + if ($expr instanceof PropertyFetch) { + return $this->getNullsafeShortCircuitingType($expr->var, $type); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($expr->class, $type); + } + + if ($expr instanceof MethodCall) { + return $this->getNullsafeShortCircuitingType($expr->var, $type); + } + + if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($expr->class, $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(); } - if (!$constantClassType->hasConstant($constantName)->yes()) { - return new ErrorType(); + return $type; + }); + } + + /** + * @param callable(Type): ?bool $typeCallback + */ + 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)) { + $hasVariable = $this->hasVariableType($expr->name); + if ($hasVariable->maybe()) { + return null; + } + + if ($result === null) { + if ($hasVariable->yes()) { + if ($expr->name === '_SESSION') { + return null; + } + + return $typeCallback($this->getVariableType($expr->name)); + } + + return false; + } + + return $result; + } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + $type = $this->getType($expr->var); + if (!$type->isOffsetAccessible()->yes()) { + return $result ?? $this->issetCheckUndefined($expr->var); + } + + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if ($hasOffsetValue->no()) { + return false; + } + + // If offset cannot be null, store this error message and see if one of the earlier offsets is. + // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. + if ($hasOffsetValue->yes()) { + $result = $typeCallback($type->getOffsetValueType($dimType)); + + if ($result !== null) { + return $this->issetCheck($expr->var, $typeCallback, $result); + } + } + + // Has offset, it is nullable + return null; + + } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) { + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + + if ($propertyReflection === null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; + } + + if (!$propertyReflection->isNative()) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; } - return $constantClassType->getConstant($constantName)->getValueType(); - } + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if (!$this->hasExpressionType($expr)->yes()) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } - 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, true)->getType($node->cond); + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); } - return $this->filterByFalseyValue($node->cond, true)->getType($node->else); + return null; } - return TypeCombinator::union( - $this->filterByTruthyValue($node->cond, true)->getType($node->cond), - $this->filterByFalseyValue($node->cond, true)->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); + if ($result !== null) { + return $result; } - return TypeCombinator::union( - $this->filterByTruthyValue($node->cond)->getType($node->if), - $this->filterByFalseyValue($node->cond)->getType($node->else) - ); - } + $result = $typeCallback($propertyReflection->getWritableType()); + if ($result !== null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheck($expr->var, $typeCallback, $result); + } - if ($node instanceof Variable && is_string($node->name)) { - if ($this->hasVariableType($node->name)->no()) { - return new ErrorType(); + if ($expr->class instanceof Expr) { + return $this->issetCheck($expr->class, $typeCallback, $result); + } } - return $this->getVariableType($node->name); + return $result; } - if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) { - return $this->getTypeFromArrayDimFetch( - $node, - $this->getType($node->dim), - $this->getType($node->var) - ); + if ($result !== null) { + return $result; } - if ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { - $methodCalledOnType = $this->getType($node->var); - $methodName = $node->name->name; - $map = function (Type $type, callable $traverse) use ($methodName, $node): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - if ($type instanceof IntersectionType) { - $returnTypes = []; - foreach ($type->getTypes() as $innerType) { - $returnType = $this->methodCallReturnType( - $type, - $innerType, - $methodName, - $node - ); - if ($returnType === null) { - continue; - } + return $typeCallback($this->getType($expr)); + } - $returnTypes[] = $returnType; - } - if (count($returnTypes) === 0) { - return new NeverType(); - } - return TypeCombinator::intersect(...$returnTypes); - } - return $this->methodCallReturnType( - $type, - $type, - $methodName, - $node - ) ?? new NeverType(); - }; - $returnType = TypeTraverser::map($methodCalledOnType, $map); - if ($returnType instanceof NeverType) { - return new ErrorType(); + private function issetCheckUndefined(Expr $expr): ?bool + { + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + $hasVariable = $this->hasVariableType($expr->name); + if (!$hasVariable->no()) { + return null; } - return $returnType; + + return false; } - if ($node instanceof Expr\StaticCall && $node->name instanceof Node\Identifier) { - if ($node->class instanceof Name) { - $staticMethodCalledOnType = new ObjectType($this->resolveName($node->class)); - } else { - $staticMethodCalledOnType = $this->getType($node->class); - if ($staticMethodCalledOnType instanceof GenericClassStringType) { - $staticMethodCalledOnType = $staticMethodCalledOnType->getGenericType(); - } + if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + $type = $this->getType($expr->var); + if (!$type->isOffsetAccessible()->yes()) { + return $this->issetCheckUndefined($expr->var); } - $methodName = $node->name->toString(); - $map = function (Type $type, callable $traverse) use ($methodName, $node): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - if ($type instanceof IntersectionType) { - $returnTypes = []; - foreach ($type->getTypes() as $innerType) { - $returnType = $this->methodCallReturnType( - $type, - $innerType, - $methodName, - $node - ); - if ($returnType === null) { - continue; - } + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); - $returnTypes[] = $returnType; - } - if (count($returnTypes) === 0) { - return new NeverType(); - } - return TypeCombinator::intersect(...$returnTypes); - } - return $this->methodCallReturnType( - $type, - $type, - $methodName, - $node - ) ?? new NeverType(); - }; - $returnType = TypeTraverser::map($staticMethodCalledOnType, $map); - if ($returnType instanceof NeverType) { - return new ErrorType(); + if (!$hasOffsetValue->no()) { + return $this->issetCheckUndefined($expr->var); } - return $returnType; + + return false; } - if ($node instanceof PropertyFetch && $node->name instanceof Node\Identifier) { - $propertyFetchedOnType = $this->getType($node->var); - $propertyName = $node->name->name; - $map = function (Type $type, callable $traverse) use ($propertyName, $node): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - if ($type instanceof IntersectionType) { - $returnTypes = []; - foreach ($type->getTypes() as $innerType) { - $returnType = $this->propertyFetchType( - $innerType, - $propertyName, - $node - ); - if ($returnType === null) { - continue; - } + if ($expr instanceof Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } - $returnTypes[] = $returnType; - } - if (count($returnTypes) === 0) { - return new NeverType(); - } - return TypeCombinator::intersect(...$returnTypes); - } - return $this->propertyFetchType( - $type, - $propertyName, - $node - ) ?? new NeverType(); - }; + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } - $returnType = TypeTraverser::map($propertyFetchedOnType, $map); - if ($returnType instanceof NeverType) { - return new ErrorType(); + return null; + } + + /** + * @param ParametersAcceptor[] $variants + */ + 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; } - return $returnType; - } - if ( - $node instanceof Expr\StaticPropertyFetch - && $node->name instanceof Node\VarLikeIdentifier - ) { - if ($node->class instanceof Name) { - $staticPropertyFetchedOnType = new ObjectType($this->resolveName($node->class)); - } else { - $staticPropertyFetchedOnType = $this->getType($node->class); - if ($staticPropertyFetchedOnType instanceof GenericClassStringType) { - $staticPropertyFetchedOnType = $staticPropertyFetchedOnType->getGenericType(); + $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(), + ); } - $staticPropertyName = $node->name->toString(); - $map = function (Type $type, callable $traverse) use ($staticPropertyName, $node): Type { - if ($type instanceof UnionType) { - return $traverse($type); + $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 ($type instanceof IntersectionType) { - $returnTypes = []; - foreach ($type->getTypes() as $innerType) { - $returnType = $this->propertyFetchType( - $innerType, - $staticPropertyName, - $node - ); - if ($returnType === null) { - continue; - } - $returnTypes[] = $returnType; + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); } - if (count($returnTypes) === 0) { - return new NeverType(); + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); } - return TypeCombinator::intersect(...$returnTypes); - } - return $this->propertyFetchType( - $type, - $staticPropertyName, - $node - ) ?? new NeverType(); - }; - - $returnType = TypeTraverser::map($staticPropertyFetchedOnType, $map); - if ($returnType instanceof NeverType) { - return new ErrorType(); - } - return $returnType; - } - - if ($node instanceof FuncCall) { - if ($node->name instanceof Expr) { - $calledOnType = $this->getType($node->name); - if ($calledOnType->isCallable()->no()) { - return new ErrorType(); } - return ParametersAcceptorSelector::selectFromArgs( - $this, - $node->args, - $calledOnType->getCallableParametersAcceptors($this) - )->getReturnType(); - } - - if (!$this->reflectionProvider->hasFunction($node->name, $this)) { - return new ErrorType(); - } - - $functionReflection = $this->reflectionProvider->getFunction($node->name, $this); - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { - if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { - continue; + $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant); + if ($impurePoint !== null) { + $impurePoints[] = $impurePoint; } - return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this); + $acceptsNamedArguments = $function->acceptsNamedArguments(); } - return ParametersAcceptorSelector::selectFromArgs( - $this, - $node->args, - $functionReflection->getVariants() - )->getReturnType(); + $parameters = $variant->getParameters(); + $closureTypes[] = new ClosureType( + $parameters, + $returnType, + $variant->isVariadic(), + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), + $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $templateTags, + $throwPoints, + $impurePoints, + [], + [], + $acceptsNamedArguments, + ); } - return new MixedType(); + return TypeCombinator::union(...$closureTypes); } - private function resolveConstantType(string $constantName, Type $constantType): Type + /** @api */ + public function getNativeType(Expr $expr): Type { - if ($constantType instanceof ConstantType && in_array($constantName, $this->dynamicConstantNames, true)) { - return $constantType->generalize(); - } - - return $constantType; + return $this->promoteNativeTypes()->getType($expr); } - public function getNativeType(Expr $expr): Type + public function getKeepVoidType(Expr $node): Type { - $key = $this->getNodeKey($expr); - - if (array_key_exists($key, $this->nativeExpressionTypes)) { - return $this->nativeExpressionTypes[$key]; - } + $clonedNode = clone $node; + $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true); - return $this->getType($expr); + return $this->getType($clonedNode); } 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->context, - $this->declareStrictTypes, - $this->constantTypes, - $this->function, - $this->namespace, - $this->variableTypes, - $this->moreSpecificTypes, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->dynamicConstantNames, - false - ); + return $this->promoteNativeTypes(); } private function promoteNativeTypes(): self { - $variableTypes = $this->variableTypes; - foreach ($this->nativeExpressionTypes as $expressionType => $type) { - if (substr($expressionType, 0, 1) !== '$') { - var_dump($expressionType); - throw new \PHPStan\ShouldNotHappenException(); - } - - $variableName = substr($expressionType, 1); - $has = $this->hasVariableType($variableName); - if ($has->no()) { - throw new \PHPStan\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->inClosureBindScopeClass, + $this->nativeExpressionTypes, + [], + [], + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - [] + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + true, ); } /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return bool + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - private function hasPropertyNativeType($propertyFetch): bool + public function hasPropertyNativeType($propertyFetch): bool { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $this); if ($propertyReflection === null) { @@ -1945,125 +2746,34 @@ private function hasPropertyNativeType($propertyFetch): bool return false; } - return !$propertyReflection->getNativeType() instanceof MixedType; + return $propertyReflection->hasNativeType(); } - protected function getTypeFromArrayDimFetch( + private function getTypeFromArrayDimFetch( Expr\ArrayDimFetch $arrayDimFetch, Type $offsetType, - Type $offsetAccessibleType + Type $offsetAccessibleType, ): Type { if ($arrayDimFetch->dim === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ((new ObjectType(\ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { + if (!$offsetAccessibleType->isArray()->yes() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { return $this->getType( new MethodCall( $arrayDimFetch->var, new Node\Identifier('offsetGet'), [ new Node\Arg($arrayDimFetch->dim), - ] - ) + ], + ), ); } 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 \PHPStan\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($leftNumberValue % $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; @@ -2079,7 +2789,7 @@ private function resolveExactName(Name $name): ?string return null; } $currentClassReflection = $this->getClassReflection(); - if ($currentClassReflection->getParentClass() !== false) { + if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); } return null; @@ -2090,18 +2800,23 @@ private function resolveExactName(Name $name): ?string return $originalClass; } + /** @api */ 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->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + return $this->inClosureBindScopeClasses[0]; + } return $this->getClassReflection()->getName(); - } elseif ($originalClass === 'parent') { + } elseif ($lowerClass === 'parent') { $currentClassReflection = $this->getClassReflection(); - if ($currentClassReflection->getParentClass() !== false) { + if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); } } @@ -2110,7 +2825,60 @@ public function resolveName(Name $name): string return $originalClass; } + /** @api */ + public function resolveTypeByName(Name $name): TypeWithClassName + { + if ($name->toLowerString() === 'static' && $this->isInClass()) { + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) { + return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0])); + } + } + + return new StaticType($this->getClassReflection()); + } + + $originalClass = $this->resolveName($name); + if ($this->isInClass()) { + if ($this->inClosureBindScopeClasses === [$originalClass]) { + if ($this->reflectionProvider->hasClass($originalClass)) { + return new ThisType($this->reflectionProvider->getClass($originalClass)); + } + return new ObjectType($originalClass); + } + + $thisType = new ThisType($this->getClassReflection()); + $ancestor = $thisType->getAncestorWithClassName($originalClass); + if ($ancestor !== null) { + return $ancestor; + } + } + + 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 */ public function getTypeFromValue($value): Type @@ -2118,37 +2886,45 @@ public function getTypeFromValue($value): Type return ConstantTypeHelper::getTypeFromValue($value); } - public function isSpecified(Expr $node): bool + /** @api */ + 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 - * @return self + * @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->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $stack + $this->currentlyAllowedUndefinedExpressions, + $stack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -2160,23 +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->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $stack + $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; } @@ -2193,49 +2973,90 @@ 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 */ + public function isInFunctionExists(string $functionName): bool + { + $expr = new FuncCall(new FullyQualified('function_exists'), [ + new Arg(new String_(ltrim($functionName, '\\'))), + ]); + + 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, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection + $namespace, + $this->expressionTypes, + $this->nativeExpressionTypes, + [], + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, ); } /** - * @param Node\Stmt\ClassMethod $classMethod - * @param TemplateTypeMap $templateTypeMap + * @api * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @return self + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2246,37 +3067,162 @@ public function enterClassMethod( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal + 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()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->enterFunctionLike( new PhpMethodFromParserNodeReflection( $this->getClassReflection(), $classMethod, + null, + $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($classMethod), - array_map(static function (Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }, $phpDocParameterTypes), + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes), $this->getRealParameterDefaultValues($classMethod), - $classMethod->returnType !== null, - $this->getFunctionType($classMethod->returnType, $classMethod->returnType === null, false), - $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, + $this->getParameterAttributes($classMethod), + $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), + $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal - ) + $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 { + if (!$this->isInClass()) { + return $type; + } + if ($type instanceof StaticType) { + $classReflection = $this->getClassReflection(); + $changedType = $type->changeBaseClass($classReflection); + if ($classReflection->isFinal() && !$type instanceof ThisType) { + $changedType = $changedType->getStaticObjectType(); + } + return $traverse($changedType); + } + + return $traverse($type); + }); + } + /** - * @param Node\FunctionLike $functionLike * @return Type[] */ private function getRealParameterTypes(Node\FunctionLike $functionLike): array @@ -2284,12 +3230,12 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array $realParameterTypes = []; foreach ($functionLike->getParams() as $parameter) { if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $realParameterTypes[$parameter->var->name] = $this->getFunctionType( $parameter->type, - $this->isParameterValueNullable($parameter), - false + $this->isParameterValueNullable($parameter) && $parameter->flags === 0, + false, ); } @@ -2297,7 +3243,6 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array } /** - * @param Node\FunctionLike $functionLike * @return Type[] */ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): array @@ -2308,7 +3253,7 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): continue; } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $realParameterDefaultValues[$parameter->var->name] = $this->getType($parameter->default); } @@ -2317,16 +3262,32 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): } /** - * @param Node\Stmt\Function_ $function - * @param TemplateTypeMap $templateTypeMap + * @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|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @return self + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2337,264 +3298,547 @@ 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( new PhpFunctionFromParserNodeReflection( $function, + $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($function), - array_map(static function (Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }, $phpDocParameterTypes), + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), $this->getRealParameterDefaultValues($function), - $function->returnType !== null, + $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, ); } private function enterFunctionLike( - PhpFunctionFromParserNodeReflection $functionReflection + PhpFunctionFromParserNodeReflection $functionReflection, + bool $preserveConstructorScope, ): self { - $variableTypes = $this->getVariableTypes(); + $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->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { + $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); + } else { + $parameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $parameterType), new AccessoryArrayListType()); + } + } + $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()) { - $parameterType = new ArrayType(new IntegerType(), $parameterType); + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { + $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); + } else { + $nativeParameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $nativeParameterType), new AccessoryArrayListType()); + } } - $variableTypes[$parameter->getName()] = VariableTypeHolder::createYes($parameterType); - $nativeExpressionTypes[sprintf('$%s', $parameter->getName())] = $parameter->getNativeType(); + $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 + $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, - $scopeClass, - $this->anonymousFunctionReflection + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $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, - $originalScope->inClosureBindScopeClass, - $this->anonymousFunctionReflection + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $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, - $thisType instanceof TypeWithClassName ? $thisType->getClassName() : null, - $this->anonymousFunctionReflection + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $thisType->getObjectClassNames(), + $this->anonymousFunctionReflection, ); } + /** @api */ public function isInClosureBind(): bool { - return $this->inClosureBindScopeClass !== null; + return $this->inClosureBindScopeClasses !== []; } /** - * @param \PhpParser\Node\Expr\Closure $closure - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters - * @return self + * @api + * @param ParameterReflection[]|null $callableParameters */ public function enterAnonymousFunction( Expr\Closure $closure, - ?array $callableParameters = null + ?array $callableParameters, + ): self + { + $anonymousFunctionReflection = $this->getType($closure); + if (!$anonymousFunctionReflection instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + + $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters); + + return $this->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + [], + $scope->inClosureBindScopeClasses, + $anonymousFunctionReflection, + true, + [], + [], + $this->inFunctionCallsStack, + false, + $this, + $this->nativeTypesPromoted, + ); + } + + /** + * @param ParameterReflection[]|null $callableParameters + */ + private function enterAnonymousFunctionWithoutReflection( + Expr\Closure $closure, + ?array $callableParameters, ): self { - $variableTypes = []; + $expressionTypes = []; + $nativeTypes = []; foreach ($closure->params as $i => $parameter) { - if ($parameter->type === null) { - if ($callableParameters === null) { - $parameterType = new MixedType(); - } elseif (isset($callableParameters[$i])) { - $parameterType = $callableParameters[$i]->getType(); + 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 = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = $lastParameter->getType(); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = new MixedType(); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = new MixedType(); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } - } else { - $isNullable = $this->isParameterValueNullable($parameter); - $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); - } - - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); } - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes( - $parameterType - ); + $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); + $expressionTypes[$paramExprString] = $holder; + $nativeTypes[$paramExprString] = $holder; } - $nativeTypes = []; + $nonRefVariableNames = []; foreach ($closure->uses as $use) { if (!is_string($use->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + 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; } - if ($this->hasVariableType($use->var->name)->no()) { + $nonRefVariableNames[$variableName] = true; + if ($this->hasVariableType($variableName)->no()) { $variableType = new ErrorType(); + $variableNativeType = new ErrorType(); } else { - $variableType = $this->getVariableType($use->var->name); - $nativeTypes[sprintf('$%s', $use->var->name)] = $this->getNativeType($use->var); + $variableType = $this->getVariableType($variableName); + $variableNativeType = $this->getNativeType($use->var); } - $variableTypes[$use->var->name] = VariableTypeHolder::createYes($variableType); + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType); } - if ($this->hasVariableType('this')->yes() && !$closure->static) { - $variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this')); + 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; + } + } + + $expressionTypes[$exprString] = $typeHolder; } - $anonymousFunctionReflection = $this->getType($closure); - if (!$anonymousFunctionReflection instanceof ClosureType) { - throw new \PHPStan\ShouldNotHappenException(); + if ($this->hasVariableType('this')->yes() && !$closure->static) { + $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, + array_merge($this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeConstantTypes(), $nativeTypes), + [], + $this->inClosureBindScopeClasses, + new TrivialParametersAcceptor(), + true, + [], [], - $this->inClosureBindScopeClass, + [], + 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): self + { + $anonymousFunctionReflection = $this->getType($arrowFunction); + if (!$anonymousFunctionReflection instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + + $scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters); + + return $this->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + $scope->conditionalExpressions, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], - $nativeTypes + [], + $this->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $this->nativeTypesPromoted, ); } - public function enterArrowFunction(Expr\ArrowFunction $arrowFunction): self + /** + * @param ParameterReflection[]|null $callableParameters + */ + private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { - $variableTypes = $this->variableTypes; - $mixed = new MixedType(); - foreach ($arrowFunction->params as $parameter) { + $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); } - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + if ($callableParameters !== null) { + if (isset($callableParameters[$i])) { + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); + } elseif (count($callableParameters) > 0) { + $lastParameter = $callableParameters[count($callableParameters) - 1]; + if ($lastParameter->isVariadic()) { + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); + } else { + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); + } + } else { + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); + } } - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes($parameterType); + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes()); } if ($arrowFunction->static) { - unset($variableTypes['this']); - } - - $anonymousFunctionReflection = $this->getType($arrowFunction); - if (!$anonymousFunctionReflection instanceof ClosureType) { - throw new \PHPStan\ShouldNotHappenException(); + $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this')); } return $this->scopeFactory->create( - $this->context, + $arrowFunctionScope->context, $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, + $arrowFunctionScope->getFunction(), + $arrowFunctionScope->getNamespace(), + $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes), + $arrowFunctionScope->nativeExpressionTypes, + $arrowFunctionScope->conditionalExpressions, + $arrowFunctionScope->inClosureBindScopeClasses, + new TrivialParametersAcceptor(), + true, + [], [], - $this->inClosureBindScopeClass, - $anonymousFunctionReflection + [], + $arrowFunctionScope->afterExtractCall, + $arrowFunctionScope->parentScope, + $this->nativeTypesPromoted, ); } @@ -2608,121 +3852,130 @@ public function isParameterValueNullable(Node\Param $parameter): bool } /** - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $type - * @param bool $isNullable - * @param bool $isVariadic - * @return Type + * @api + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type { if ($isNullable) { return TypeCombinator::addNull( - $this->getFunctionType($type, false, $isVariadic) + $this->getFunctionType($type, false, $isVariadic), ); } if ($isVariadic) { - return new ArrayType(new IntegerType(), $this->getFunctionType( + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { + return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType( + $type, + false, + false, + )); + } + + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getFunctionType( $type, false, - false - )); + false, + )), new AccessoryArrayListType()); } - if ($type === null) { - return new MixedType(); - } elseif ($type instanceof Name) { + + if ($type instanceof Name) { $className = (string) $type; $lowercasedClassName = strtolower($className); - if ($this->isInClass() && in_array($lowercasedClassName, ['self', 'static'], true)) { - $className = $this->getClassReflection()->getName(); - } elseif ( - $lowercasedClassName === 'parent' - ) { - if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== false) { + if ($lowercasedClassName === 'parent') { + if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== null) { return new ObjectType($this->getClassReflection()->getParentClass()->getName()); } - return new NonexistentParentClassType(); - } - return new ObjectType($className); - } elseif ($type instanceof Node\NullableType) { - return $this->getFunctionType($type->type, true, $isVariadic); - } elseif ($type instanceof Node\UnionType) { - $types = []; - foreach ($type->types as $unionTypeType) { - $types[] = $this->getFunctionType($unionTypeType, false, false); + return new NonexistentParentClassType(); } + } - return TypeCombinator::union(...$types); + 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; } - $type = $type->name; - if ($type === 'string') { - return new StringType(); - } elseif ($type === 'int') { - return new IntegerType(); - } elseif ($type === 'bool') { - return new BooleanType(); - } elseif ($type === 'float') { - return new FloatType(); - } elseif ($type === 'callable') { - return new CallableType(); - } elseif ($type === 'array') { - return new ArrayType(new MixedType(), new MixedType()); - } elseif ($type === 'iterable') { - return new IterableType(new MixedType(), new MixedType()); - } elseif ($type === 'void') { - return new VoidType(); - } elseif ($type === 'object') { - return new ObjectWithoutClassType(); - } elseif ($type === 'mixed') { - return new MixedType(true); + $result = TypeCombinator::intersect($nativeType, $inferredType); + if (TypeCombinator::containsNull($nativeType)) { + return TypeCombinator::addNull($result); } - return new MixedType(); + return $result; } - public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self + public function enterMatch(Expr\Match_ $expr): self { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType()); - $scope->nativeExpressionTypes[sprintf('$%s', $valueName)] = $nativeIterateeType->getIterableValueType(); + if ($expr->cond instanceof Variable) { + return $this; + } + if ($expr->cond instanceof AlwaysRememberedExpr) { + $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(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self + { + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $valueName, + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + 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(); + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $keyName, + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + TrinaryLogic::createYes(), + ); + + if ($iterateeType->isArray()->yes()) { + $scope = $scope->assignExpression( + new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + ); + } return $scope; } - /** - * @param \PhpParser\Node\Name[] $classes - * @param string|null $variableName - * @return self - */ - public function enterCatch(array $classes, ?string $variableName): self + public function enterCatchType(Type $catchType, ?string $variableName): self { if ($variableName === null) { return $this; } - $type = TypeCombinator::union(...array_map(static function (string $class): ObjectType { - return new ObjectType($class); - }, $classes)); - return $this->assignVariable( $variableName, - TypeCombinator::intersect($type, new ObjectType(\Throwable::class)) + TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), + TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), + TrinaryLogic::createYes(), ); } @@ -2732,20 +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->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $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 @@ -2754,398 +4016,734 @@ 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->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $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 */ public function isInExpressionAssign(Expr $expr): bool { $exprString = $this->getNodeKey($expr); 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 \PHPStan\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 = \Nette\Utils\Strings::matchAll((string) $key, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); - if ($matches === []) { - continue; - } + $exprString = $this->getNodeKey($expr); + $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions; + $currentlyAllowedUndefinedExpressions[$exprString] = true; - $matches = array_column($matches, 0); + $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; - if (!in_array($variableString, $matches, true)) { - continue; - } + return $scope; + } - unset($moreSpecificTypeHolders[$key]); - } + 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, - $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; - unset($nativeTypes[sprintf('$%s', $expr->name)]); - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - [], - $nativeTypes + $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)]), ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $constantArrays = TypeUtils::getConstantArrays($this->getType($expr->var)); - if (count($constantArrays) > 0) { - $unsetArrays = []; - $dimType = $this->getType($expr->dim); - foreach ($constantArrays as $constantArray) { - $unsetArrays[] = $constantArray->unsetOffset($dimType); - } - return $this->specifyExpressionType( - $expr->var, - TypeCombinator::union(...$unsetArrays) + + 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), + ), ); } - - $args = [new Node\Arg($expr->var)]; - - return $this->invalidateExpression($expr->var) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('count'), $args)) - ->invalidateExpression(new FuncCall(new Name('count'), $args)); } - return $this; + return $scope->invalidateExpression($expr); } - public function specifyExpressionType(Expr $expr, Type $type): 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->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack - ); + $loweredConstName = strtolower($expr->name->toString()); + if (in_array($loweredConstName, ['true', 'false', 'null'], true)) { + return $this; + } } - $exprString = $this->getNodeKey($expr); + if ($expr instanceof FuncCall && $expr->name instanceof Name && $type->isFalse()->yes()) { + $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this); + if ($functionName !== null && in_array(strtolower($functionName), [ + 'is_dir', + 'is_file', + 'file_exists', + ], true)) { + return $this; + } + } $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(); + } - if ($expr instanceof Variable && is_string($expr->name)) { - $variableName = $expr->name; - - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = VariableTypeHolder::createYes($type); - - $nativeTypes = $this->nativeExpressionTypes; - $nativeTypes[sprintf('$%s', $variableName)] = $type; - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $nativeTypes, - $this->inFunctionCallsStack - ); - } 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) - ); + $scope = $scope->specifyExpressionType( + $expr->var, + TypeCombinator::intersect( + TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)), + new HasOffsetValueType($dimType, $type), + ), + $scope->getNativeType($expr->var), + $certainty, + ); + } } } - return $scope->addMoreSpecificTypes([ - $exprString => $type, - ]); + 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 || $expr instanceof Expr\StaticPropertyFetch) { + if ($expr instanceof PropertyFetch) { + $scope = $this->invalidateExpression($expr) + ->invalidateMethodsOnExpression($expr->var); + } elseif ($expr instanceof Expr\StaticPropertyFetch) { + $scope = $this->invalidateExpression($expr); + } elseif ($expr instanceof Variable) { $scope = $this->invalidateExpression($expr); } - return $scope->specifyExpressionType($expr, $type); + 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 $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); } public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $invalidated = false; $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; - if (Strings::startsWith($exprString, $exprStringToInvalidate)) { - if ($exprString === $exprStringToInvalidate && $requireMoreCharacters) { - continue; - } - $nextLetter = substr($exprString, strlen($exprStringToInvalidate), 1); - if (Strings::match($nextLetter, '#[a-zA-Z_0-9\x7f-\xff]#') === null) { - unset($moreSpecificTypeHolders[$exprString]); - continue; - } + + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $requireMoreCharacters)) { + continue; + } + + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + $newConditionalExpressions = []; + foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { + if (count($holders) === 0) { + continue; } - $matches = \Nette\Utils\Strings::matchAll($exprString, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); - if ($matches === []) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $holders[array_key_first($holders)]->getTypeHolder()->getExpr())) { + $invalidated = true; continue; } + 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) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $newConditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + [], + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, bool $requireMoreCharacters = false): bool + { + 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(); + $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; + } + + if (!$node instanceof $expressionToInvalidateClass) { + return false; + } + + $nodeString = $this->getNodeKey($node); + + return $nodeString === $exprStringToInvalidate; + }); + + if ($found === null) { + return false; + } + + if ($this->phpVersion->supportsReadOnlyProperties() && $expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier && $requireMoreCharacters) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection !== null) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { + return false; + } + } + } - $matches = array_column($matches, 0); + 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; + } - if (!in_array($exprStringToInvalidate, $matches, true)) { + return $this->getNodeKey($node->var) === $exprStringToInvalidate; + }); + if ($found === null) { continue; } - unset($moreSpecificTypeHolders[$exprString]); + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + if (!$invalidated) { + return $this; } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->inClosureBindScopeClass, + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions + $this->currentlyAssignedExpressions, + $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; } /** - * @param \PhpParser\Node\Expr $expr - * @param bool $defaultHandleFunctions - * @return \PHPStan\Analyser\MutatingScope + * @api + * @return MutatingScope */ - public function filterByTruthyValue(Expr $expr, bool $defaultHandleFunctions = false): Scope + public function filterByTruthyValue(Expr $expr): Scope { - $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy(), $defaultHandleFunctions); - return $this->filterBySpecifiedTypes($specifiedTypes); + $exprString = $this->getNodeKey($expr); + if (array_key_exists($exprString, $this->truthyScopes)) { + return $this->truthyScopes[$exprString]; + } + + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy()); + $scope = $this->filterBySpecifiedTypes($specifiedTypes); + $this->truthyScopes[$exprString] = $scope; + + return $scope; } /** - * @param \PhpParser\Node\Expr $expr - * @param bool $defaultHandleFunctions - * @return \PHPStan\Analyser\MutatingScope + * @api + * @return MutatingScope */ - public function filterByFalseyValue(Expr $expr, bool $defaultHandleFunctions = false): Scope + public function filterByFalseyValue(Expr $expr): Scope { - $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey(), $defaultHandleFunctions); - return $this->filterBySpecifiedTypes($specifiedTypes); + $exprString = $this->getNodeKey($expr); + if (array_key_exists($exprString, $this->falseyScopes)) { + return $this->falseyScopes[$exprString]; + } + + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey()); + $scope = $this->filterBySpecifiedTypes($specifiedTypes); + $this->falseyScopes[$exprString] = $scope; + + return $scope; } 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 { - $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']; + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric }); $scope = $this; + $specifiedExpressions = []; foreach ($typeSpecifications as $typeSpecification) { $expr = $typeSpecification['expr']; $type = $typeSpecification['type']; - if ($typeSpecification['sure']) { - $scope = $scope->specifyExpressionType($expr, $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $this->getType($expr))); - if ($expr instanceof Variable && is_string($expr->name)) { - $scope->nativeExpressionTypes[sprintf('$%s', $expr->name)] = $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $this->getNativeType($expr)); + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); + + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); + } + + continue; + } + + if ($typeSpecification['sure']) { + if ($specifiedTypes->shouldOverwrite()) { + $scope = $scope->assignExpression($expr, $type, $type); + } else { + $scope = $scope->addTypeToExpression($expr, $type); } } else { $scope = $scope->removeTypeFromExpression($expr, $type); } + $specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr)); + } + + $conditions = []; + foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { + foreach ($conditionalExpressions as $conditionalExpression) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { + continue 2; + } + } + + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } } - return $scope; + foreach ($conditions as $conditionalExprString => $expressions) { + $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty()); + if ($certainty->no()) { + unset($scope->expressionTypes[$conditionalExprString]); + } else { + $type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions)); + + $scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes) + ? new ExpressionTypeHolder( + $scope->expressionTypes[$conditionalExprString]->getExpr(), + TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type), + TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty), + ) + : $expressions[0]->getTypeHolder(); + } + } + + 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, + ); } - public function exitFirstLevelStatements(): self + /** + * @param ConditionalExpressionHolder[] $conditionalExpressionHolders + */ + public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self { + $conditionalExpressions = $this->conditionalExpressions; + $conditionalExpressions[$exprString] = $conditionalExpressionHolders; return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, - $this->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, - false, + $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } - public function isInFirstLevelStatement(): bool + public function exitFirstLevelStatements(): self { - return $this->inFirstLevelStatement; - } + if (!$this->inFirstLevelStatement) { + return $this; + } - /** - * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - * @param Type[] $types - * @return self - */ - private function addMoreSpecificTypes(array $types): self - { - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach ($types as $exprString => $type) { - $moreSpecificTypeHolders[$exprString] = VariableTypeHolder::createYes($type); + if ($this->scopeOutOfFirstLevelStatement !== null) { + return $this->scopeOutOfFirstLevelStatement; } - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->inClosureBindScopeClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, + 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 */ + public function isInFirstLevelStatement(): bool + { + return $this->inFirstLevelStatement; } public function mergeWith(?self $otherScope): self @@ -3153,165 +4751,305 @@ public function mergeWith(?self $otherScope): self if ($otherScope === null) { return $this; } + $ourExpressionTypes = $this->expressionTypes; + $theirExpressionTypes = $otherScope->expressionTypes; - $variableHolderToType = static function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; - + $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); + $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions); + $conditionalExpressions = $this->createConditionalExpressions( + $conditionalExpressions, + $ourExpressionTypes, + $theirExpressionTypes, + $mergedExpressionTypes, + ); + $conditionalExpressions = $this->createConditionalExpressions( + $conditionalExpressions, + $theirExpressionTypes, + $ourExpressionTypes, + $mergedExpressionTypes, + ); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, $this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) - )), $this->getFunction(), $this->getNamespace(), - $this->mergeVariableHolders($this->getVariableTypes(), $otherScope->getVariableTypes()), - $this->mergeVariableHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes), - $this->inClosureBindScopeClass, + $mergedExpressionTypes, + $this->mergeVariableHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes), + $conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) - ), static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - })), - [] + [], + [], + $this->afterExtractCall && $otherScope->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } /** - * @param VariableTypeHolder[] $ourVariableTypeHolders - * @param VariableTypeHolder[] $theirVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $otherConditionalExpressions + * @return array + */ + private function intersectConditionalExpressions(array $otherConditionalExpressions): array + { + $newConditionalExpressions = []; + foreach ($this->conditionalExpressions as $exprString => $holders) { + if (!array_key_exists($exprString, $otherConditionalExpressions)) { + continue; + } + + $otherHolders = $otherConditionalExpressions[$exprString]; + foreach (array_keys($holders) as $key) { + if (!array_key_exists($key, $otherHolders)) { + continue 2; + } + } + + $newConditionalExpressions[$exprString] = $holders; + } + + return $newConditionalExpressions; + } + + /** + * @param array $conditionalExpressions + * @param array $ourExpressionTypes + * @param array $theirExpressionTypes + * @param array $mergedExpressionTypes + * @return array + */ + private function createConditionalExpressions( + array $conditionalExpressions, + array $ourExpressionTypes, + array $theirExpressionTypes, + array $mergedExpressionTypes, + ): array + { + $newVariableTypes = $ourExpressionTypes; + foreach ($theirExpressionTypes as $exprString => $holder) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + + if (!$mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { + continue; + } + + unset($newVariableTypes[$exprString]); + } + + $typeGuards = []; + foreach ($newVariableTypes as $exprString => $holder) { + if (!$holder->getCertainty()->yes()) { + continue; + } + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + if ($mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { + continue; + } + + $typeGuards[$exprString] = $holder; + } + + if (count($typeGuards) === 0) { + return $conditionalExpressions; + } + + foreach ($newVariableTypes as $exprString => $holder) { + if ( + array_key_exists($exprString, $mergedExpressionTypes) + && $mergedExpressionTypes[$exprString]->equals($holder) + ) { + continue; + } + + $variableTypeGuards = $typeGuards; + unset($variableTypeGuards[$exprString]); + + if (count($variableTypeGuards) === 0) { + continue; + } + + $conditionalExpression = new ConditionalExpressionHolder($variableTypeGuards, $holder); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; + } + + foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) { + if (array_key_exists($exprString, $ourExpressionTypes)) { + continue; + } + + $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; + } + + return $conditionalExpressions; + } + + /** + * @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; + } + + $expr = $variableTypeHolder->getExpr(); + if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) { continue; } - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } return $intersectedVariableTypeHolders; } - public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + public function mergeInitializedProperties(self $calledMethodScope): self { - $variableHolderToType = static function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; + $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, $this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $finallyScope->constantTypes), - array_map($typeToVariableHolder, $originalFinallyScope->constantTypes) - )), $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->inClosureBindScopeClass, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - array_map($variableHolderToType, $this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $finallyScope->nativeExpressionTypes), - array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes) - )), - [] + [], + [], + $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, array $finallyVariableTypeHolders, - array $originalVariableTypeHolders + 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 self $closureScope - * @param self|null $prevScope - * @param Expr\ClosureUse[] $byRefUses - * @return self + * @param Node\ClosureUse[] $byRefUses */ public function processClosureScope( self $closureScope, ?self $prevScope, - array $byRefUses + array $byRefUses, ): self { - $variableTypes = $this->variableTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $expressionTypes = $this->expressionTypes; if (count($byRefUses) === 0) { return $this; } foreach ($byRefUses as $use) { if (!is_string($use->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableName = $use->var->name; + $variableExprString = '$' . $variableName; if (!$closureScope->hasVariableType($variableName)->yes()) { - $variableTypes[$variableName] = VariableTypeHolder::createYes(new NullType()); + $holder = ExpressionTypeHolder::createYes($use->var, new NullType()); + $expressionTypes[$variableExprString] = $holder; + $nativeExpressionTypes[$variableExprString] = $holder; continue; } @@ -3321,145 +5059,140 @@ 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); + $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->inClosureBindScopeClass, + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], - $this->nativeExpressionTypes, - $this->inFunctionCallsStack + [], + $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, - $this->inClosureBindScopeClass, + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $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 function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; - $nativeTypes = array_map($variableHolderToType, $this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) - )); - return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, $this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) - )), $this->getFunction(), $this->getNamespace(), $variableTypeHolders, - $moreSpecificTypes, - $this->inClosureBindScopeClass, + $nativeTypes, + $this->conditionalExpressions, + $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 $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()), - $variableTypeHolder->getCertainty() + $variableTypeHolders[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0), + $variableTypeHolder->getCertainty(), ); } return $variableTypeHolders; } - private function generalizeType(Type $a, Type $b): Type + private function generalizeType(Type $a, Type $b, int $depth): Type { if ($a->equals($b)) { return $a; @@ -3471,6 +5204,7 @@ private function generalizeType(Type $a, Type $b): Type $constantStrings = ['a' => [], 'b' => []]; $constantArrays = ['a' => [], 'b' => []]; $generalArrays = ['a' => [], 'b' => []]; + $integerRanges = ['a' => [], 'b' => []]; $otherTypes = []; foreach ([ @@ -3494,14 +5228,18 @@ private function generalizeType(Type $a, Type $b): Type $constantStrings[$key][] = $type; continue; } - if ($type instanceof ConstantArrayType) { + if ($type->isConstantArray()->yes()) { $constantArrays[$key][] = $type; continue; } - if ($type instanceof ArrayType) { + if ($type->isArray()->yes()) { $generalArrays[$key][] = $type; continue; } + if ($type instanceof IntegerRangeType) { + $integerRanges[$key][] = $type; + continue; + } $otherTypes[] = $type; } @@ -3509,15 +5247,16 @@ private function generalizeType(Type $a, Type $b): Type $resultTypes = []; foreach ([ - $constantIntegers, $constantFloats, $constantBooleans, $constantStrings, ] as $constantTypes) { if (count($constantTypes['a']) === 0) { + if (count($constantTypes['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$constantTypes['b']); + } continue; - } - if (count($constantTypes['b']) === 0) { + } elseif (count($constantTypes['b']) === 0) { $resultTypes[] = TypeCombinator::union(...$constantTypes['a']); continue; } @@ -3529,7 +5268,7 @@ private function generalizeType(Type $a, Type $b): Type continue; } - $resultTypes[] = TypeUtils::generalizeType($constantTypes['a'][0]); + $resultTypes[] = TypeCombinator::union(...$constantTypes['a'], ...$constantTypes['b'])->generalize(GeneralizePrecision::moreSpecific()); } if (count($constantArrays['a']) > 0) { @@ -3538,26 +5277,44 @@ private 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) - ) + $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) { + $resultTypes[] = TypeCombinator::union(...$constantArrays['b']); } if (count($generalArrays['a']) > 0) { @@ -3569,16 +5326,14 @@ private 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 = $this->getArrayDepth($aArrays[0]); - $bDepth = $this->getArrayDepth($bArrays[0]); + $aDepth = self::getArrayDepth($aValueType) + $depth; + $bDepth = self::getArrayDepth($bValueType) + $depth; if ( ($aDepth > 2 || $bDepth > 2) && abs($aDepth - $bDepth) > 0 @@ -3588,27 +5343,175 @@ private 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']); + } + + if (count($constantIntegers['a']) > 0) { + if (count($constantIntegers['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$constantIntegers['a']); + } else { + $constantIntegersA = TypeCombinator::union(...$constantIntegers['a']); + $constantIntegersB = TypeCombinator::union(...$constantIntegers['b']); + + if ($constantIntegersA->equals($constantIntegersB)) { + $resultTypes[] = $constantIntegersA; + } else { + $min = null; + $max = null; + foreach ($constantIntegers['a'] as $int) { + if ($min === null || $int->getValue() < $min) { + $min = $int->getValue(); + } + if ($max !== null && $int->getValue() <= $max) { + continue; + } + + $max = $int->getValue(); + } + + $gotGreater = false; + $gotSmaller = false; + foreach ($constantIntegers['b'] as $int) { + if ($int->getValue() > $max) { + $gotGreater = true; + } + if ($int->getValue() >= $min) { + continue; + } + + $gotSmaller = true; + } + + if ($gotGreater && $gotSmaller) { + $resultTypes[] = new IntegerType(); + } elseif ($gotGreater) { + $resultTypes[] = IntegerRangeType::fromInterval($min, null); + } elseif ($gotSmaller) { + $resultTypes[] = IntegerRangeType::fromInterval(null, $max); + } else { + $resultTypes[] = TypeCombinator::union($constantIntegersA, $constantIntegersB); + } + } + } + } elseif (count($constantIntegers['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$constantIntegers['b']); + } + + if (count($integerRanges['a']) > 0) { + if (count($integerRanges['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$integerRanges['a']); + } else { + $integerRangesA = TypeCombinator::union(...$integerRanges['a']); + $integerRangesB = TypeCombinator::union(...$integerRanges['b']); + + if ($integerRangesA->equals($integerRangesB)) { + $resultTypes[] = $integerRangesA; + } else { + $min = null; + $max = null; + foreach ($integerRanges['a'] as $range) { + if ($range->getMin() === null) { + $rangeMin = PHP_INT_MIN; + } else { + $rangeMin = $range->getMin(); + } + if ($range->getMax() === null) { + $rangeMax = PHP_INT_MAX; + } else { + $rangeMax = $range->getMax(); + } + + if ($min === null || $rangeMin < $min) { + $min = $rangeMin; + } + if ($max !== null && $rangeMax <= $max) { + continue; + } + + $max = $rangeMax; + } + + $gotGreater = false; + $gotSmaller = false; + foreach ($integerRanges['b'] as $range) { + if ($range->getMin() === null) { + $rangeMin = PHP_INT_MIN; + } else { + $rangeMin = $range->getMin(); + } + if ($range->getMax() === null) { + $rangeMax = PHP_INT_MAX; + } else { + $rangeMax = $range->getMax(); + } + + if ($rangeMax > $max) { + $gotGreater = true; + } + if ($rangeMin >= $min) { + continue; + } + + $gotSmaller = true; + } + + if ($min === PHP_INT_MIN) { + $min = null; + } + if ($max === PHP_INT_MAX) { + $max = null; + } + + if ($gotGreater && $gotSmaller) { + $resultTypes[] = new IntegerType(); + } elseif ($gotGreater) { + $resultTypes[] = IntegerRangeType::fromInterval($min, null); + } elseif ($gotSmaller) { + $resultTypes[] = IntegerRangeType::fromInterval(null, $max); + } else { + $resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB); + } + } } + } elseif (count($integerRanges['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$integerRanges['b']); } - return TypeCombinator::union(...$resultTypes, ...$otherTypes); + $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 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++; } @@ -3621,64 +5524,116 @@ public function equals(self $otherScope): bool return false; } - if (!$this->compareVariableTypeHolders($this->variableTypes, $otherScope->variableTypes)) { + if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) { return false; } + return $this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes); + } - if (!$this->compareVariableTypeHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes)) { + /** + * @param array $variableTypeHolders + * @param array $otherVariableTypeHolders + */ + private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool + { + if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) { return false; } + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$variableExprString])) { + return false; + } - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; + if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) { + return false; + } - $nativeExpressionTypesResult = $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) - ); + if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$variableExprString]->getType())) { + return false; + } - if (!$nativeExpressionTypesResult) { - return false; + unset($otherVariableTypeHolders[$variableExprString]); } - return $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) - ); + return true; + } + + private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int + { + while ( + $expr instanceof BinaryOp\BooleanOr + || $expr instanceof BinaryOp\LogicalOr + || $expr instanceof BinaryOp\BooleanAnd + || $expr instanceof BinaryOp\LogicalAnd + ) { + return $this->getBooleanExpressionDepth($expr->left, $depth + 1); + } + + return $depth; } /** - * @param VariableTypeHolder[] $variableTypeHolders - * @param VariableTypeHolder[] $otherVariableTypeHolders - * @return bool + * @api + * @deprecated Use canReadProperty() or canWriteProperty() */ - private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool + public function canAccessProperty(PropertyReflection $propertyReflection): bool { - foreach ($variableTypeHolders as $name => $variableTypeHolder) { - if (!isset($otherVariableTypeHolders[$name])) { - return false; + 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(); } - if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$name]->getCertainty())) { - return false; + // protected set + + if ( + $classReflection->getName() === $propertyDeclaringClass->getName() + || $classReflection->isSubclassOfClass($propertyDeclaringClass) + ) { + return true; } - if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$name]->getType())) { - return false; + return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; } - unset($otherVariableTypeHolders[$name]); + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } } - return count($otherVariableTypeHolders) === 0; - } + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); + } - public function canAccessProperty(PropertyReflection $propertyReflection): bool - { - return $this->canAccessClassMember($propertyReflection); + return false; } + /** @api */ public function canCallMethod(MethodReflection $methodReflection): bool { if ($this->canAccessClassMember($methodReflection)) { @@ -3688,7 +5643,8 @@ public function canCallMethod(MethodReflection $methodReflection): bool return $this->canAccessClassMember($methodReflection->getPrototype()); } - public function canAccessConstant(ConstantReflection $constantReflection): bool + /** @api */ + public function canAccessConstant(ClassConstantReflection $constantReflection): bool { return $this->canAccessClassMember($constantReflection); } @@ -3699,29 +5655,39 @@ private function canAccessClassMember(ClassMemberReflection $classMemberReflecti return true; } - if ($this->inClosureBindScopeClass !== null && $this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - $currentClassReflection = $this->reflectionProvider->getClass($this->inClosureBindScopeClass); - } elseif ($this->isInClass()) { - $currentClassReflection = $this->getClassReflection(); - } else { - return false; - } + $classMemberDeclaringClass = $classMemberReflection->getDeclaringClass(); + $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classMemberDeclaringClass) { + if ($classMemberReflection->isPrivate()) { + return $classReflection->getName() === $classMemberDeclaringClass->getName(); + } - $classReflectionName = $classMemberReflection->getDeclaringClass()->getName(); - if ($classMemberReflection->isPrivate()) { - return $currentClassReflection->getName() === $classReflectionName; - } + // protected - // protected + if ( + $classReflection->getName() === $classMemberDeclaringClass->getName() + || $classReflection->isSubclassOfClass($classMemberDeclaringClass) + ) { + return true; + } - if ( - $currentClassReflection->getName() === $classReflectionName - || $currentClassReflection->isSubclassOf($classReflectionName) - ) { - return true; + return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } + + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } } - return $classMemberReflection->getDeclaringClass()->isSubclassOf($currentClassReflection->getName()); + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); + } + + return false; } /** @@ -3730,31 +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->nativeExpressionTypes as $exprString => $nativeTypeHolder) { + $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe()); + $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise()); } - foreach ($this->constantTypes as $name => $type) { - $key = sprintf('const %s', $name); - $descriptions[$key] = $type->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)) { @@ -3762,224 +5750,511 @@ 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), new Node\Identifier($constructorMethod->getName()), - $node->args + $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); + + 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[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($constructorMethod, $methodCall, $this); + $resolvedTypes[] = $resolvedType; + } } if (count($resolvedTypes) > 0) { return TypeCombinator::union(...$resolvedTypes); } + $methodResult = $this->getType($methodCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + return $methodResult; + } + + $objectType = $isStatic ? new StaticType($classReflection) : new ObjectType($resolvedClassName, null, $classReflection); if (!$classReflection->isGeneric()) { - return new ObjectType($resolvedClassName); + return $objectType; + } + + $assignedToProperty = $node->getAttribute(NewAssignedToPropertyVisitor::ATTRIBUTE_NAME); + if ($assignedToProperty !== null) { + $constructorVariant = $constructorMethod->getOnlyVariant(); + $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + $originalClassTemplateTypes = $classTemplateTypes; + foreach ($constructorVariant->getParameters() as $parameter) { + TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$classTemplateTypes): Type { + if ($type instanceof TemplateType && array_key_exists($type->getName(), $classTemplateTypes)) { + $classTemplateType = $classTemplateTypes[$type->getName()]; + if ($classTemplateType instanceof TemplateType && $classTemplateType->getScope()->equals($type->getScope())) { + unset($classTemplateTypes[$type->getName()]); + } + return $type; + } + + return $traverse($type); + }); + } + + if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { + $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) { + 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(), + ); } - if ($constructorMethod instanceof DummyConstructorReflection || $constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if ($constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$constructorMethod->getDeclaringClass()->isGeneric()) { + 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(), + ); + } + $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, + [], + ); + } + + $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, + [], + ); + } + + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()); + return new GenericObjectType( + $resolvedClassName, + $types, + null, + $classReflection->withTypes($types)->asFinal(), + ); + } + + $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(), + ); + } + $newParentTypeClassReflection = $newParentTypeClassReflections[0]; + + $ancestorClassReflection = $ancestorClassReflections[0]; + $ancestorMapping = []; + foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + + $ancestorMapping[$typeName] = $templateType; + } + + $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 ($isStatic) { + return new GenericStaticType( + $classReflection, + $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)), + null, + [], + ); + } + + $types = $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)); return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()) + $types, + null, + $classReflection->withTypes($types)->asFinal(), ); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, - $methodCall->args, - $constructorMethod->getVariants() + $methodCall->getArgs(), + $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), ); - return new GenericObjectType( + $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); + $newGenericType = new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()) + $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(); + } + + return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + } + + return $traverse($type); + }); } - private function getTypeToInstantiateForNew(Type $type): Type + private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type { - $decideType = static function (Type $type): ?Type { - if ($type instanceof TypeWithClassName) { - return $type; - } - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); - } - return null; - }; + if ($typeWithMethod instanceof UnionType) { + $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes()); + } - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $decidedType = $decideType($innerType); - if ($decidedType === null) { - return new MixedType(); - } + if (!$typeWithMethod->hasMethod($methodName)->yes()) { + return null; + } - $types[] = $decidedType; - } + return $typeWithMethod; + } - return TypeCombinator::union(...$types); + /** @api */ + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + { + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { + return null; } - $decidedType = $decideType($type); - if ($decidedType === null) { - return new MixedType(); + return $type->getMethod($methodName, $this); + } + + public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + { + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { + return null; } - return $decidedType; + return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod(); } /** - * @param \PHPStan\Type\Type $calledOnType - * @param \PHPStan\Type\Type $typeWithMethod - * @param string $methodName - * @param MethodCall|\PhpParser\Node\Expr\StaticCall $methodCall - * @return \PHPStan\Type\Type|null + * @param MethodCall|Node\Expr\StaticCall $methodCall */ - private function methodCallReturnType(Type $calledOnType, Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type + private function methodCallReturnType(Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type { - if (!$typeWithMethod->hasMethod($methodName)->yes()) { + $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); + } - if ($typeWithMethod instanceof TypeWithClassName) { - $resolvedTypes = []; - - if ($methodCall instanceof MethodCall) { - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicMethodReturnTypeExtensionsForClass($typeWithMethod->getClassName()) as $dynamicMethodReturnTypeExtension) { + $resolvedTypes = []; + 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($typeWithMethod->getClassName()) as $dynamicStaticMethodReturnTypeExtension) { + foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) { if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($methodReflection)) { 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); - } } - $methodReturnType = ParametersAcceptorSelector::selectFromArgs( - $this, - $methodCall->args, - $methodReflection->getVariants() - )->getReturnType(); - - if ($methodCall instanceof MethodCall) { - $calledOnThis = $calledOnType instanceof StaticType && $this->isInClass(); - } else { - if (!$methodCall->class instanceof Name) { - $calledOnThis = false; - } else { - $calledOnThis = in_array(strtolower($methodCall->class->toString()), ['self', 'static', 'parent'], true) && $this->isInClass(); - } + if (count($resolvedTypes) > 0) { + return $this->transformVoidToNull(TypeCombinator::union(...$resolvedTypes), $methodCall); } - $transformedCalledOnType = TypeTraverser::map($calledOnType, function (Type $type, callable $traverse) use ($calledOnThis): Type { - if ($type instanceof StaticType) { - if ($calledOnThis && $this->isInClass()) { - return $traverse($type->changeBaseClass($this->getClassReflection())); - } - if ($this->isInClass()) { - return $traverse($type->changeBaseClass($this->getClassReflection())->getStaticObjectType()); - } - } - - return $traverse($type); - }); - - return TypeTraverser::map($methodReturnType, function (Type $returnType, callable $traverse) use ($transformedCalledOnType, $calledOnThis): Type { - if ($returnType instanceof StaticType) { - if ($calledOnThis && $this->isInClass()) { - return $traverse($returnType->changeBaseClass($this->getClassReflection())); - } + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); + } - return $traverse($transformedCalledOnType); - } + /** @api */ + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection + { + if ($typeWithProperty instanceof UnionType) { + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes()); + } + if (!$typeWithProperty->hasProperty($propertyName)->yes()) { + return null; + } - return $traverse($returnType); - }); + return $typeWithProperty->getProperty($propertyName, $this); } /** - * @param \PHPStan\Type\Type $fetchedOnType - * @param string $propertyName - * @param PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return \PHPStan\Type\Type|null + * @param PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Expr $propertyFetch): ?Type { - if (!$fetchedOnType->hasProperty($propertyName)->yes()) { + $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { return null; } - $propertyReflection = $fetchedOnType->getProperty($propertyName, $this); - if ($this->isInExpressionAssign($propertyFetch)) { - $propertyType = $propertyReflection->getWritableType(); - } else { - $propertyType = $propertyReflection->getReadableType(); + return $propertyReflection->getWritableType(); } - if ($propertyFetch instanceof PropertyFetch) { - $fetchedOnThis = $fetchedOnType instanceof StaticType && $this->isInClass(); - } else { - if (!$propertyFetch->class instanceof Name) { - $fetchedOnThis = false; - } else { - $fetchedOnThis = in_array(strtolower($propertyFetch->class->toString()), ['self', 'static', 'parent'], true) && $this->isInClass(); + return $propertyReflection->getReadableType(); + } + + public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection + { + if ($typeWithConstant instanceof UnionType) { + $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes()); + } + if (!$typeWithConstant->hasConstant($constantName)->yes()) { + return null; + } + + return $typeWithConstant->getConstant($constantName); + } + + /** + * @return array + */ + private function getConstantTypes(): array + { + $constantTypes = []; + foreach ($this->expressionTypes as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof ConstFetch) { + continue; } + $constantTypes[$exprString] = $typeHolder; } + return $constantTypes; + } - $transformedFetchedOnType = TypeTraverser::map($fetchedOnType, function (Type $type, callable $traverse) use ($fetchedOnThis): Type { - if ($type instanceof StaticType) { - if ($fetchedOnThis && $this->isInClass()) { - return $traverse($type->changeBaseClass($this->getClassReflection())); - } - if ($this->isInClass()) { - return $traverse($type->changeBaseClass($this->getClassReflection())->getStaticObjectType()); - } + private function getGlobalConstantType(Name $name): ?Type + { + $fetches = []; + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])); + } + + $fetches[] = new ConstFetch(new FullyQualified($name->toString())); + $fetches[] = new ConstFetch($name); + + foreach ($fetches as $constFetch) { + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->getType($constFetch); } + } - return $traverse($type); - }); + return null; + } - return TypeTraverser::map($propertyType, function (Type $propertyType, callable $traverse) use ($transformedFetchedOnType, $fetchedOnThis): Type { - if ($propertyType instanceof StaticType) { - if ($fetchedOnThis && $this->isInClass()) { - return $traverse($propertyType->changeBaseClass($this->getClassReflection())); - } + /** + * @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; + } - return $traverse($transformedFetchedOnType); + 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; } + } - return $traverse($propertyType); - }); + return $iteratee->getIterableKeyType(); + } + + 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; + } + } + + return $iteratee->getIterableValueType(); + } + + public function getPhpVersion(): PhpVersions + { + $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); + if ($constType !== null) { + return new PhpVersions($constType); + } + + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); + } + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } } diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index e0de734e7d..2ce18d91be 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -5,33 +5,61 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; - -class NameScope +use function array_key_exists; +use function array_merge; +use function array_shift; +use function count; +use function explode; +use function implode; +use function ltrim; +use function sprintf; +use function str_starts_with; +use function strtolower; + +/** + * @api + */ +final class NameScope { - private ?string $namespace; + private TemplateTypeMap $templateTypeMap; - /** @var string[] alias(string) => fullName(string) */ - private array $uses; + /** + * @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, private array $constUses = [], private ?string $typeAliasClassName = null) + { + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + } - private ?string $className; + public function getNamespace(): ?string + { + return $this->namespace; + } - private ?string $functionName; + /** + * @return array + */ + public function getUses(): array + { + return $this->uses; + } - private TemplateTypeMap $templateTypeMap; + public function hasUseAlias(string $name): bool + { + return isset($this->uses[strtolower($name)]); + } /** - * @param string|null $namespace - * @param string[] $uses alias(string) => fullName(string) - * @param string|null $className + * @return array */ - public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null) + public function getConstUses(): array { - $this->namespace = $namespace; - $this->uses = $uses; - $this->className = $className; - $this->functionName = $functionName; - $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + return $this->constUses; } public function getClassName(): ?string @@ -39,9 +67,14 @@ public function getClassName(): ?string return $this->className; } + public function getClassNameForTypeAlias(): ?string + { + return $this->typeAliasClassName ?? $this->className; + } + public function resolveStringName(string $name): string { - if (strpos($name, '\\') === 0) { + if (str_starts_with($name, '\\')) { return ltrim($name, '\\'); } @@ -62,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) { @@ -91,6 +155,10 @@ public function resolveTemplateTypeName(string $name): ?Type public function withTemplateTypeMap(TemplateTypeMap $map): self { + if ($map->isEmpty() && $this->templateTypeMap->isEmpty()) { + return $this; + } + return new self( $this->namespace, $this->uses, @@ -98,24 +166,74 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self $this->functionName, new TemplateTypeMap(array_merge( $this->templateTypeMap->getTypes(), - $map->getTypes() - )) + $map->getTypes(), + )), + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self + 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( - $properties['namespace'], - $properties['uses'], - $properties['className'], - $properties['functionName'], - $properties['templateTypeMap'] + $this->namespace, + $this->uses, + $className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } + public function unsetTemplateType(string $name): self + { + $map = $this->templateTypeMap; + if (!$map->hasType($name)) { + return $this; + } + + return new self( + $this->namespace, + $this->uses, + $this->className, + $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, $this->constUses); + } + + public function shouldBypassTypeAliases(): bool + { + return $this->bypassTypeAliases; + } + + public function hasTypeAlias(string $alias): bool + { + return array_key_exists($alias, $this->typeAliasesMap); + } + } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 97ff79c4a4..58aa455c4a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2,11 +2,18 @@ namespace PHPStan\Analyser; +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; @@ -14,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; @@ -28,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_; @@ -37,155 +46,249 @@ 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; +use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; +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; +use PHPStan\Node\BooleanOrNode; +use PHPStan\Node\BreaklessWhileLoopNode; +use PHPStan\Node\CatchWithUnthrownExceptionNode; +use PHPStan\Node\ClassConstantsNode; +use PHPStan\Node\ClassMethodsNode; +use PHPStan\Node\ClassPropertiesNode; +use PHPStan\Node\ClassPropertyNode; +use PHPStan\Node\ClassStatementsGatherer; 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; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InClosureNode; +use PHPStan\Node\InForeachNode; use PHPStan\Node\InFunctionNode; +use PHPStan\Node\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\Tag\ParamTag; +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\PassedByReference; -use PHPStan\Reflection\Php\DummyParameter; +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\CallableType; use PHPStan\Type\ClosureType; -use PHPStan\Type\CommentHelper; 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\ErrorType; use PHPStan\Type\FileTypeMapper; +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\UnionType; -use Roave\BetterReflection\Reflection\Adapter\ReflectionClass; -use Roave\BetterReflection\Reflector\ClassReflector; -use Roave\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; -use Roave\BetterReflection\SourceLocator\Located\LocatedSource; - -class NodeScopeResolver +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_shift; +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; + +final class NodeScopeResolver { private const LOOP_SCOPE_ITERATIONS = 3; private const GENERALIZE_AFTER_ITERATION = 1; - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private ClassReflector $classReflector; - - private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private \PHPStan\File\FileHelper $fileHelper; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private bool $polluteScopeWithLoopInitialAssignments; - - private bool $polluteCatchScopeWithTryAssignments; - - private bool $polluteScopeWithAlwaysIterableForeach; + /** @var bool[] filePath(string) => bool(true) */ + private array $analysedFiles = []; - /** @var string[][] className(string) => methods(string[]) */ - private array $earlyTerminatingMethodCalls; + /** @var array */ + private array $earlyTerminatingMethodNames; - /** @var array */ - private array $earlyTerminatingFunctionCalls; + /** @var array */ + private array $calledMethodStack = []; - /** @var bool[] filePath(string) => bool(true) */ - private array $analysedFiles; + /** @var array */ + private array $calledMethodResults = []; /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param ClassReflector $classReflector - * @param Parser $parser - * @param FileTypeMapper $fileTypeMapper - * @param PhpDocInheritanceResolver $phpDocInheritanceResolver - * @param FileHelper $fileHelper - * @param TypeSpecifier $typeSpecifier - * @param bool $polluteScopeWithLoopInitialAssignments - * @param bool $polluteCatchScopeWithTryAssignments - * @param bool $polluteScopeWithAlwaysIterableForeach * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls + * @param string[] $universalObjectCratesClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassReflector $classReflector, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - Parser $parser, - FileTypeMapper $fileTypeMapper, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - FileHelper $fileHelper, - TypeSpecifier $typeSpecifier, - bool $polluteScopeWithLoopInitialAssignments, - bool $polluteCatchScopeWithTryAssignments, - bool $polluteScopeWithAlwaysIterableForeach, - array $earlyTerminatingMethodCalls, - array $earlyTerminatingFunctionCalls + 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, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classReflector = $classReflector; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->parser = $parser; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->fileHelper = $fileHelper; - $this->typeSpecifier = $typeSpecifier; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; - $this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments; - $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; - $this->earlyTerminatingMethodCalls = $earlyTerminatingMethodCalls; - $this->earlyTerminatingFunctionCalls = $earlyTerminatingFunctionCalls; + $earlyTerminatingMethodNames = []; + foreach ($this->earlyTerminatingMethodCalls as $methodNames) { + foreach ($methodNames as $methodName) { + $earlyTerminatingMethodNames[strtolower($methodName)] = true; + } + } + $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; } /** + * @api * @param string[] $files */ public function setAnalysedFiles(array $files): void @@ -194,108 +297,161 @@ public function setAnalysedFiles(array $files): void } /** - * @param \PhpParser\Node[] $nodes - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @api + * @param Node[] $nodes + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processNodes( array $nodes, MutatingScope $scope, - \Closure $nodeCallback + 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); + } + } - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + /** + * @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 = []; + + 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); } /** - * @param \PhpParser\Node $parentNode - * @param \PhpParser\Node\Stmt[] $stmts - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult + * @api + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processStmtNodes( Node $parentNode, array $stmts, MutatingScope $scope, - \Closure $nodeCallback + 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 + $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() - ), - $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); + $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); } @@ -303,63 +459,71 @@ public function processStmtNodes( } /** - * @param \PhpParser\Node\Stmt $stmt - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processStmtNode( Node\Stmt $stmt, MutatingScope $scope, - \Closure $nodeCallback + callable $nodeCallback, + StatementContext $context, ): StatementResult { if ( - $stmt instanceof Echo_ - || ( - $stmt instanceof Node\Stmt\Expression - && !$stmt->expr instanceof Assign && !$stmt->expr instanceof AssignRef - ) - || $stmt instanceof If_ - || $stmt instanceof While_ - || $stmt instanceof Switch_ + !$stmt instanceof Static_ + && !$stmt instanceof Foreach_ + && !$stmt instanceof Node\Stmt\Global_ + && !$stmt instanceof Node\Stmt\Property + && !$stmt instanceof Node\Stmt\ClassConst + && !$stmt instanceof Node\Stmt\Const_ ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); - } elseif ( - $stmt instanceof Throw_ - || $stmt instanceof Return_ - ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr); + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } if ($stmt instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ( $scope->isInTrait() && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString()) ) { $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); + if ($methodReflection instanceof NativeMethodReflection) { + 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; @@ -367,18 +531,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; - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal] = $this->getPhpDocs($scope, $stmt); + $throwPoints = []; + $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, @@ -388,52 +571,88 @@ 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 = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements): void { + $gatheredYieldStatements = []; + $executionEnds = []; + $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; + } + 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, - $statementResult + $gatheredYieldStatements, + $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $functionImpurePoints), + $functionReflection, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal] = $this->getPhpDocs($scope, $stmt); + $throwPoints = []; + $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 ($phpDocReturnType !== null) { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); - $phpDocReturnType = TypeTraverser::map($phpDocReturnType, static function (Type $type, callable $traverse) use ($classReflection): Type { - if ($type instanceof StaticType) { - return $traverse($type->changeBaseClass($classReflection)); - } - - return $traverse($type); - }); + if (!$isDeprecated) { + [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); } + $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; + $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct'; + $methodScope = $scope->enterClassMethod( $stmt, $templateTypeMap, @@ -443,158 +662,427 @@ private function processStmtNode( $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal + $isFinal, + $isPure, + $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $isConstructor, ); - $nodeCallback(new InClassMethodNode($stmt), $methodScope); + + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $classReflection = $scope->getClassReflection(); + + if ($isConstructor) { + foreach ($stmt->params as $param) { + if ($param->flags === 0 && $param->hooks === []) { + continue; + } + + if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') { + throw new ShouldNotHappenException(); + } + $phpDoc = null; + if ($param->getDocComment() !== null) { + $phpDoc = $param->getDocComment()->getText(); + } + $nodeCallback(new ClassPropertyNode( + $param->var->name, + $param->flags, + $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) { + $methodReflection = $methodScope->getFunction(); + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope); + } if ($stmt->stmts !== null) { $gatheredReturnStatements = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements): void { + $gatheredYieldStatements = []; + $executionEnds = []; + $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; + } + 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, - $statementResult + $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, $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, $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, $stmt->expr, - TypeSpecifierContext::createNull() + TypeSpecifierContext::createNull(), )); $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($earlyTerminationExpr !== null) { return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ]); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } + 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)) { - $nodeToReflection = new NodeToReflection(); - $betterReflectionClass = $nodeToReflection->__invoke( - $this->classReflector, - $stmt, - new LocatedSource(FileReader::read($scope->getFile()), $scope->getFile()), - $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null, - null - ); - if (!$betterReflectionClass instanceof \Roave\BetterReflection\Reflection\ReflectionClass) { - throw new \PHPStan\ShouldNotHappenException(); - } - $classReflection = new ClassReflection( - $this->reflectionProvider, - $this->fileTypeMapper, - $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), - $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), - $betterReflectionClass->getName(), - new ReflectionClass($betterReflectionClass), - null, - null, - null, - sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()) - ); - $this->reflectionProvider->hasClass($classReflection->getName()); + $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); } elseif ($stmt instanceof Class_) { if ($stmt->name === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + if (!$stmt->isAnonymous()) { + $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); + } else { + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); } - $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); $classScope = $scope->enterClass($classReflection); $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); } else { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + $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']; + }); } - $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $nodeCallback); + $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 = []; + $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); + $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( + $propertyName, + $stmt->flags, + $nativePropertyType, + $prop->default, + $docComment, + $phpDocType, + false, + false, + $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; - 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()); - return new StatementResult($result->getScope(), $result->hasYield(), true, [ - new StatementExitPoint($stmt, $scope), - ]); } 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 = false; + $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(); - $hasYield = $branchScopeStatementResult->hasYield(); + 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; } $scope = $condResult->getFalseyScope(); @@ -603,10 +1091,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 @@ -619,15 +1109,23 @@ 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; } @@ -637,19 +1135,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(); } } @@ -658,38 +1165,60 @@ private function processStmtNode( $finalScope = $scope; } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints); + 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_) { - $scope = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); - $bodyScope = $this->enterForeach($scope, $stmt); - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($scope); - $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; - } + $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_([]), + ); + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { + $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); + } + $nodeCallback(new InForeachNode($stmt), $scope); + $originalScope = $scope; + $bodyScope = $scope; - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); - } - $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; + } - $bodyScope = $bodyScope->mergeWith($scope); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } + + $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); @@ -698,66 +1227,93 @@ 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()) { - $finalScope = $finalScope->mergeWith($scope); } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); // get types from finalScope, but don't create new variables } + 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); + } + return new StatementResult( $finalScope, - $finalScopeResult->hasYield(), + $finalScopeResult->hasYield() || $condResult->hasYield(), $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 = $bodyScope->generalizeWith($prevScope); - } - $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(); - $finalScope = $finalScopeResult->getScope(); - foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + $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); + + $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(); + $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) { $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; @@ -774,168 +1330,287 @@ private function processStmtNode( $finalScope = $finalScope->mergeWith($condScope); } + $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + if (!$neverIterates) { + $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); + } + return new StatementResult( $finalScope, - $finalScopeResult->hasYield(), + $finalScopeResult->hasYield() || $condResult->hasYield(), $isAlwaysTerminating, - [] + $finalScopeResult->getExitPointsForOuterLoop(), + $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof Do_) { $finalScope = null; $bodyScope = $scope; $count = 0; - 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; - } + $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; + } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); - } - $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(); - $condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + $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); if ($alwaysIterates) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; } else { $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); } - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); if ($finalScope === null) { $finalScope = $scope; } if (!$alwaysTerminating) { - $finalScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getFalseyScope(); + $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, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); } foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - return new StatementResult($finalScope, $bodyScopeResult->hasYield(), $alwaysTerminating, []); + return new StatementResult( + $finalScope, + $bodyScopeResult->hasYield() || $hasYield, + $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) { - $initScope = $this->processExprNode($initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $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) { - $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); + $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { + }, ExpressionContext::createDeep()); + $initScope = $condResult->getScope(); + $condResultScope = $condResult->getScope(); + + if ($condExpr === $lastCondExpr) { + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); + } + + $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) { - $bodyScope = $this->processExprNode($loopExpr, $bodyScope, static function (): void { - }, ExpressionContext::createTopLevel())->getScope(); - } + 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 = $bodyScope->generalizeWith($prevScope); - } - $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); } + + $loopScope = $finalScope; foreach ($stmt->loop as $loopExpr) { - $finalScope = $this->processExprNode($loopExpr, $finalScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + } + $finalScope = $finalScope->generalizeWith($loopScope); + + if ($lastCondExpr !== null) { + $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } + foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - if ($this->polluteScopeWithLoopInitialAssignments) { - $scope = $initScope; + if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + if ($this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $initScope; + } else { + $finalScope = $scope; + } + + } elseif ($isIterableAtLeastOnce->maybe()) { + if ($this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $finalScope->mergeWith($initScope); + } else { + $finalScope = $finalScope->mergeWith($scope); + } + } else { + if (!$this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $finalScope->mergeWith($scope); + } } - $finalScope = $finalScope->mergeWith($scope); + 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(), false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, []); + return new StatementResult( + $finalScope, + $finalScopeResult->hasYield() || $hasYield, + $isAlwaysTerminating, + $finalScopeResult->getExitPointsForOuterLoop(), + array_merge($throwPoints, $finalScopeResult->getThrowPoints()), + array_merge($impurePoints, $finalScopeResult->getImpurePoints()), + ); } elseif ($stmt instanceof Switch_) { - $scope = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $condResult->getScope(); $scopeForBranches = $scope; $finalScope = null; $prevScope = null; $hasDefaultCase = false; $alwaysTerminating = true; - $hasYield = false; + $hasYield = $condResult->hasYield(); + $exitPointsForOuterLoop = []; + $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $fullCondExpr = null; + $defaultCondExprs = []; foreach ($stmt->cases as $caseNode) { if ($caseNode->cond !== null) { $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); - $scopeForBranches = $this->processExprNode($caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep())->getScope(); - $branchScope = $scopeForBranches->filterByTruthyValue($condExpr); + $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr); + $defaultCondExprs[] = new BinaryOp\NotEqual($stmt->cond, $caseNode->cond); + $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); + $scopeForBranches = $caseResult->getScope(); + $hasYield = $hasYield || $caseResult->hasYield(); + $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints()); + $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; + $fullCondExpr = null; $branchScope = $scopeForBranches; + $defaultConditions = $this->createBooleanAndFromExpressions($defaultCondExprs); + if ($defaultConditions !== null) { + $branchScope = $this->processExprNode($stmt, $defaultConditions, $scope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope()->filterByTruthyValue($defaultConditions); + } } $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(); foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $alwaysTerminating = false; $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); } + $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); @@ -945,7 +1620,9 @@ private function processStmtNode( } } - if (!$hasDefaultCase) { + $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType; + + if (!$hasDefaultCase && !$exhaustive) { $alwaysTerminating = false; } @@ -954,17 +1631,18 @@ 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, []); + 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(); - $tryScope = $branchScope; - $exitPoints = []; $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; + + $exitPoints = []; + $finallyExitPoints = []; $alwaysTerminating = $branchScopeResult->isAlwaysTerminating(); $hasYield = $branchScopeResult->hasYield(); @@ -974,312 +1652,728 @@ private function processStmtNode( $finallyScope = null; } foreach ($branchScopeResult->getExitPoints() as $exitPoint) { + $finallyExitPoints[] = $exitPoint; + if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) { + continue; + } if ($finallyScope !== null) { $finallyScope = $finallyScope->mergeWith($exitPoint->getScope()); } $exitPoints[] = $exitPoint; } + $throwPoints = $branchScopeResult->getThrowPoints(); + $impurePoints = $branchScopeResult->getImpurePoints(); + $throwPointsForLater = []; + $pastCatchTypes = new NeverType(); + foreach ($stmt->catches as $catchNode) { $nodeCallback($catchNode, $scope); - if (!$this->polluteCatchScopeWithTryAssignments) { - $catchScopeResult = $this->processCatchNode($catchNode, $scope->mergeWith($tryScope), $nodeCallback); - $catchScopeForFinally = $catchScopeResult->getScope(); - } else { - $catchScopeForFinally = $this->processCatchNode($catchNode, $tryScope, $nodeCallback)->getScope(); - $catchScopeResult = $this->processCatchNode($catchNode, $scope->mergeWith($tryScope), static function (): void { - }); + + $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 = []; + $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; + } + + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + $matchingCatchTypes[$catchTypeIndex] = true; + } + } + + // 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; + } + } + } + + // 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; + } + } + } + + // include previously removed throw points + if (count($matchingThrowPoints) === 0) { + if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) { + if (!$originalThrowPoint->canContainAnyThrowable()) { + continue; + } + + $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 ($newThrowPoint->getType() instanceof NeverType) { + continue; + } + + $newThrowPoints[] = $newThrowPoint; + } + $throwPoints = $newThrowPoints; + + $catchScope = null; + foreach ($matchingThrowPoints as $matchingThrowPoint) { + if ($catchScope === null) { + $catchScope = $matchingThrowPoint->getScope(); + } else { + $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope()); + } + } + + $variableName = null; + if ($catchNode->var !== null) { + if (!is_string($catchNode->var->name)) { + throw new ShouldNotHappenException(); + } + + $variableName = $catchNode->var->name; } + $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) { $finallyScope = $finallyScope->mergeWith($catchScopeForFinally); } foreach ($catchScopeResult->getExitPoints() as $exitPoint) { + $finallyExitPoints[] = $exitPoint; + if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) { + continue; + } if ($finallyScope !== null) { $finallyScope = $finallyScope->mergeWith($exitPoint->getScope()); } $exitPoints[] = $exitPoint; } + + foreach ($catchThrowPoints as $catchThrowPoint) { + if ($finallyScope === null) { + continue; + } + $finallyScope = $finallyScope->mergeWith($catchThrowPoint->getScope()); + } } if ($finalScope === null) { $finalScope = $scope; } - if ($finallyScope !== null && $stmt->finally !== null) { + foreach ($throwPoints as $throwPoint) { + if ($finallyScope === null) { + continue; + } + $finallyScope = $finallyScope->mergeWith($throwPoint->getScope()); + } + + 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) { + $nodeCallback(new FinallyExitPointsNode( + $finallyResult->getExitPoints(), + $finallyExitPoints, + ), $scope); + } $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints); + 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 \PHPStan\ShouldNotHappenException(); + 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; - $comment = CommentHelper::getDocComment($stmt); + $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'static', + 'static variable', + true, + ), + ]; + + $vars = []; foreach ($stmt->vars as $var) { - $scope = $this->processStmtNode($var, $scope, $nodeCallback)->getScope(); - if ($comment === null || !is_string($var->var->name)) { - continue; + if (!is_string($var->var->name)) { + throw new ShouldNotHappenException(); } - $scope = $this->processVarAnnotation($scope, $var->var->name, $comment, false); + + 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; } - } elseif ($stmt instanceof StaticVar) { + + $scope = $this->processVarAnnotation($scope, $vars, $stmt); + } elseif ($stmt instanceof Node\Stmt\Const_) { $hasYield = false; - if (!is_string($stmt->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $throwPoints = []; + $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 = []; + $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, []); + return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints); } /** - * @param Node\Stmt\Catch_ $catchNode - * @param MutatingScope $catchScope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult + * @return array{bool, string|null} */ - private function processCatchNode( - Node\Stmt\Catch_ $catchNode, - MutatingScope $catchScope, - \Closure $nodeCallback - ): StatementResult + private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array { - $variableName = null; - if ($catchNode->var !== null) { - if (!is_string($catchNode->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } + $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; + } - $variableName = $catchNode->var->name; - } + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } - $catchScope = $catchScope->enterCatch($catchNode->types, $variableName); - return $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope, $nodeCallback); - } + if ($argName->toString() !== 'message') { + continue; + } - private function lookForEnterVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope - { - if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { - $scope = $scope->enterExpressionAssign($expr); - } - if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static function (MutatingScope $scope, Expr $expr): MutatingScope { - return $scope->enterExpressionAssign($expr); - }); + $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext); + break; + } + } } - return $scope; - } - - private function lookForExitVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope - { - $scope = $scope->exitExpressionAssign($expr); - if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static function (MutatingScope $scope, Expr $expr): MutatingScope { - return $scope->exitExpressionAssign($expr); - }); + if ($deprecatedDescriptionType !== null) { + $constantStrings = $deprecatedDescriptionType->getConstantStrings(); + if (count($constantStrings) === 1) { + $deprecatedDescription = $constantStrings[0]->getValue(); + } } - return $scope; + return [$isDeprecated, $deprecatedDescription]; } /** - * @param MutatingScope $scope - * @param Expr $expr - * @param \Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback - * @return MutatingScope + * @return ThrowPoint[]|null */ - private function lookForVariableAssignCallback(MutatingScope $scope, Expr $expr, \Closure $callback): MutatingScope + private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array { - if ($expr instanceof Variable) { - $scope = $callback($scope, $expr); - } elseif ($expr instanceof ArrayDimFetch) { - while ($expr instanceof ArrayDimFetch) { - $expr = $expr->var; + foreach ($statement->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; } - $scope = $this->lookForVariableAssignCallback($scope, $expr, $callback); - } elseif ($expr instanceof PropertyFetch) { - $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_) { - foreach ($expr->items as $item) { - if ($item === null) { - continue; + $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, + $comment->getText(), + ); + + $throwsTag = $resolvedPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwsType = $throwsTag->getType(); + if ($throwsType->isVoid()->yes()) { + return []; } - $scope = $this->lookForVariableAssignCallback($scope, $item->value, $callback); + return [ThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; } } - return $scope; + return null; } - private function ensureNonNullability(MutatingScope $scope, Expr $expr, bool $findMethods): EnsuredNonNullabilityResult + private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { - $exprToSpecify = $expr; - $specifiedExpressions = []; - while ( - $exprToSpecify instanceof PropertyFetch - || $exprToSpecify instanceof StaticPropertyFetch - || ( - $findMethods && ( - $exprToSpecify instanceof MethodCall - || $exprToSpecify instanceof StaticCall - ) - ) - ) { - if ( - $exprToSpecify instanceof PropertyFetch - || $exprToSpecify instanceof MethodCall - ) { - $exprToSpecify = $exprToSpecify->var; - } elseif ($exprToSpecify->class instanceof Expr) { - $exprToSpecify = $exprToSpecify->class; - } else { - break; - } - - $exprType = $scope->getType($exprToSpecify); - $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); - if ($exprType->equals($exprTypeWithoutNull)) { - continue; - } + if (!$this->reflectionProvider->hasClass($className)) { + return $this->createAstClassReflection($stmt, $className, $scope); + } - $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType); + $defaultClassReflection = $this->reflectionProvider->getClass($className); + if ($defaultClassReflection->getFileName() !== $scope->getFile()) { + return $this->createAstClassReflection($stmt, $className, $scope); + } - $scope = $scope->specifyExpressionType($exprToSpecify, $exprTypeWithoutNull); + $startLine = $defaultClassReflection->getNativeReflection()->getStartLine(); + if ($startLine !== $stmt->getStartLine()) { + return $this->createAstClassReflection($stmt, $className, $scope); } - return new EnsuredNonNullabilityResult($scope, $specifiedExpressions); + return $defaultClassReflection; } - /** - * @param MutatingScope $scope - * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions - * @return MutatingScope - */ - private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope + private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { - foreach ($specifiedExpressions as $specifiedExpressionResult) { - $scope = $scope->specifyExpressionType($specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType()); + $nodeToReflection = new NodeToReflection(); + $betterReflectionClass = $nodeToReflection->__invoke( + $this->reflector, + $stmt, + new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()), + $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null, + ); + if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) { + throw new ShouldNotHappenException(); } - return $scope; + $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true); + + 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 findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr + private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { - if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && count($this->earlyTerminatingMethodCalls) > 0) { - if ($expr->name instanceof Expr) { - return null; - } + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); + } - if ($expr instanceof MethodCall) { - $methodCalledOnType = $scope->getType($expr->var); - } else { - if ($expr->class instanceof Name) { - $methodCalledOnType = $scope->getFunctionType($expr->class, false, false); - } else { - $methodCalledOnType = $scope->getType($expr->class); + private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope + { + 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 lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope + { + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { + $scope = $callback($scope, $expr); + } + + if ($expr instanceof ArrayDimFetch) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); + } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { + $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->lookForExpressionCallback($scope, $item->value, $callback); } + } - $directClassNames = TypeUtils::getDirectClassNames($methodCalledOnType); - foreach ($directClassNames as $referencedClass) { - if (!$this->reflectionProvider->hasClass($referencedClass)) { - continue; + return $scope; + } + + private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult + { + $exprType = $scope->getType($exprToSpecify); + $isNull = $exprType->isNull(); + if ($isNull->yes()) { + return new EnsuredNonNullabilityResult($scope, []); + } + + // keep certainty + $certainty = TrinaryLogic::createYes(); + $hasExpressionType = $originalScope->hasExpressionType($exprToSpecify); + if (!$hasExpressionType->no()) { + $certainty = $hasExpressionType; + } + + $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); + if ($exprType->equals($exprTypeWithoutNull)) { + $originalExprType = $originalScope->getType($exprToSpecify); + if (!$originalExprType->equals($exprTypeWithoutNull)) { + $originalNativeType = $originalScope->getNativeType($exprToSpecify); + + return new EnsuredNonNullabilityResult($scope, [ + new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty), + ]); + } + 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): EnsuredNonNullabilityResult + { + $specifiedExpressions = []; + $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; + } + return $result->getScope(); + }); + + return new EnsuredNonNullabilityResult($scope, $specifiedExpressions); + } + + /** + * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions + */ + private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope + { + foreach ($specifiedExpressions as $specifiedExpressionResult) { + $scope = $scope->specifyExpressionType( + $specifiedExpressionResult->getExpression(), + $specifiedExpressionResult->getOriginalType(), + $specifiedExpressionResult->getOriginalNativeType(), + $specifiedExpressionResult->getCertainty(), + ); + } + + return $scope; + } + + private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr + { + if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) { + if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { + if ($expr instanceof MethodCall) { + $methodCalledOnType = $scope->getType($expr->var); + } else { + if ($expr->class instanceof Name) { + $methodCalledOnType = $scope->resolveTypeByName($expr->class); + } else { + $methodCalledOnType = $scope->getType($expr->class); + } } - $classReflection = $this->reflectionProvider->getClass($referencedClass); - foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { - if (!isset($this->earlyTerminatingMethodCalls[$className])) { + foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { continue; } - if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) { - return $expr; + $classReflection = $this->reflectionProvider->getClass($referencedClass); + foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { + if (!isset($this->earlyTerminatingMethodCalls[$className])) { + continue; + } + + if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) { + return $expr; + } } } } } - if ($expr instanceof FuncCall && count($this->earlyTerminatingFunctionCalls) > 0) { - if ($expr->name instanceof Expr) { - return null; - } - + if ($expr instanceof FuncCall && $expr->name instanceof Name) { if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) { return $expr; } } - if ($expr instanceof Exit_) { + if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) { + return $expr; + } + + $exprType = $scope->getType($expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; } @@ -1287,141 +2381,234 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } /** - * @param \PhpParser\Node\Expr $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param \PHPStan\Analyser\ExpressionContext $context - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processExprNode(Expr $expr, MutatingScope $scope, \Closure $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); + } 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); + } else { + throw new ShouldNotHappenException(); + } + + return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context); + } + $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); 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) { - if (!$expr->var instanceof Array_ && !$expr->var instanceof List_) { - $result = $this->processAssignVar( - $scope, - $expr->var, - $expr->expr, - $nodeCallback, - $context, - function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { - if ($expr instanceof AssignRef) { - $scope = $scope->enterExpressionAssign($expr->expr); + $result = $this->processAssignVar( + $scope, + $stmt, + $expr->var, + $expr->expr, + $nodeCallback, + $context, + 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 ($expr->var instanceof Variable && is_string($expr->var->name)) { - $context = $context->enterRightSideAssign( - $expr->var->name, - $scope->getType($expr->expr) + if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'propertyAssignByRef', + 'property assignment by reference', + false, ); } - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $scope = $result->getScope(); - - if ($expr instanceof AssignRef) { - $scope = $scope->exitExpressionAssign($expr->expr); - } - - return new ExpressionResult($scope, $hasYield); - }, - true - ); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $varChangedScope = false; - if ($expr->var instanceof Variable && is_string($expr->var->name)) { - $comment = CommentHelper::getDocComment($expr); - if ($comment !== null) { - $scope = $this->processVarAnnotation($scope, $expr->var->name, $comment, false, $varChangedScope); - } - } - - if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); - } - } else { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $scope = $result->getScope(); - foreach ($expr->var->items as $arrayItem) { - if ($arrayItem === null) { - continue; + $scope = $scope->enterExpressionAssign($expr->expr); } - $itemScope = $scope; - if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) { - $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); + if ($expr->var instanceof Variable && is_string($expr->var->name)) { + $context = $context->enterRightSideAssign( + $expr->var->name, + $scope->getType($expr->expr), + $scope->getNativeType($expr->expr), + ); } - $itemScope = $this->lookForEnterVariableAssign($itemScope, $arrayItem->value); - - $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); - } - $scope = $this->lookForArrayDestructuringArray($scope, $expr->var, $scope->getType($expr->expr)); - $comment = CommentHelper::getDocComment($expr); - if ($comment !== null) { - foreach ($expr->var->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } - if (!$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) { - continue; - } - $varChangedScope = false; - $scope = $this->processVarAnnotation($scope, $arrayItem->value->name, $comment, true, $varChangedScope); - if ($varChangedScope) { - continue; - } + $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(); - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); + if ($expr instanceof AssignRef) { + $scope = $scope->exitExpressionAssign($expr->expr); } + + 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, $stmt, $varChangedScope); + if (!$varChangedScope) { + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } } } elseif ($expr instanceof Expr\AssignOp) { $result = $this->processAssignVar( $scope, + $stmt, $expr->var, $expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { - return $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 + $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) { - $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $nameType = $scope->getType($expr->name); + if (!$nameType->isCallable()->no()) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $nameType->getCallableParametersAcceptors($scope), + null, + ); + } + + $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->args, - $functionReflection->getVariants() + $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->args, $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 ($functionReflection !== null) { + $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); + if ($functionThrowPoint !== null) { + $throwPoints[] = $functionThrowPoint; + } + } else { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + } + + if ( + $parametersAcceptor instanceof ClosureType && count($parametersAcceptor->getImpurePoints()) > 0 + && $scope->isInClass() + ) { + $scope = $scope->invalidateExpression(new Variable('this'), true); + } if ( - isset($functionReflection) + $functionReflection !== null && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true) ) { $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), [])) @@ -1431,544 +2618,1837 @@ 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->args) >= 1 + && count($expr->getArgs()) >= 1 ) { - $arrayArg = $expr->args[0]->value; - $constantArrays = TypeUtils::getConstantArrays($scope->getType($arrayArg)); - if (count($constantArrays) > 0) { - $resultArrayTypes = []; + $arrayArg = $expr->getArgs()[0]->value; - 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->invalidateExpression($arrayArg)->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$resultArrayTypes) - ); - } else { - $arrays = TypeUtils::getAnyArrays($scope->getType($arrayArg)); - if (count($arrays) > 0) { - $scope = $scope->invalidateExpression($arrayArg)->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->args) >= 2 + && count($expr->getArgs()) >= 2 ) { - $argumentTypes = []; - foreach (array_slice($expr->args, 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; - } + $arrayType = $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr); + $arrayNativeType = $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr); - $argumentTypes[] = $callArgType; - } + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType); + } - $arrayArg = $expr->args[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); - } + 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()); + } - $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); - } + if ( + $functionReflection !== null + && $functionReflection->getName() === 'shuffle' + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $scope->getType($arrayArg)->shuffleArray(), + $scope->getNativeType($arrayArg)->shuffleArray(), + ); + } + + if ( + $functionReflection !== null + && $functionReflection->getName() === 'array_splice' + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $arrayArgType = $scope->getType($arrayArg); + $valueType = $arrayArgType->getIterableValueType(); + if (count($expr->getArgs()) >= 4) { + $replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray(); + $valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType()); + } + $scope = $scope->invalidateExpression($arrayArg)->assignExpression( + $arrayArg, + new ArrayType($arrayArgType->getIterableKeyType(), $valueType), + new ArrayType($arrayArgType->getIterableKeyType(), $valueType), + ); + } + + 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)), + ); + } - $defaultArrayType = $defaultArrayBuilder->getArray(); + 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)), + ); + } - $arrayTypes = []; + 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) { - $arrayType = $defaultArrayType; 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]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; + $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; } - $arrayType = $arrayType->setOffsetValueType($keyType, $valueType); } - $arrayTypes[] = $arrayType; } - - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$arrayTypes) - ); + foreach ($properties as $name => $type) { + $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays); + $scope = $scope->assignVariable($name, $type, $type, $optional ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes()); + } + } else { + $scope = $scope->afterExtractCall(); } } if ( - isset($functionReflection) - && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) + $functionReflection !== null + && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true) + ) { + $scope = $scope->afterClearstatcacheCall(); + } + + if ( + $functionReflection !== null + && str_starts_with($functionReflection->getName(), 'openssl') ) { - $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType())); + $scope = $scope->afterOpenSslCall($functionReflection->getName()); } + } elseif ($expr instanceof MethodCall) { $originalScope = $scope; if ( ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction) && $expr->name instanceof Node\Identifier && strtolower($expr->name->name) === 'call' - && isset($expr->args[0]) + && isset($expr->getArgs()[0]) ) { - $closureCallScope = $scope->enterClosureCall($scope->getType($expr->args[0]->value)); + $closureCallScope = $scope->enterClosureCall( + $scope->getType($expr->getArgs()[0]->value), + $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) { - $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $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; - if ($calledOnType->hasMethod($methodName)->yes()) { - $methodReflection = $calledOnType->getMethod($methodName, $scope); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection !== null) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $expr->args, - $methodReflection->getVariants() + $expr->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); + + $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; + } } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); - $scope = $result->getScope(); - if ($methodReflection !== null && $methodReflection->hasSideEffects()->yes()) { - $scope = $scope->invalidateExpression($expr->var, true); + + 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, + ); } - $hasYield = $hasYield || $result->hasYield(); - } elseif ($expr instanceof StaticCall) { - if ($expr->class instanceof Expr) { - $scope = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep())->getScope(); + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; } - $parametersAcceptor = null; - $methodReflection = null; - $hasYield = false; - if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $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; - } - if ($classReflection->hasMethod($methodName)) { - $methodReflection = $classReflection->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->args, - $methodReflection->getVariants() - ); - if ( - $classReflection->getName() === 'Closure' - && strtolower($methodName) === 'bind' - ) { - $thisType = null; - if (isset($expr->args[1])) { - $argType = $scope->getType($expr->args[1]->value); - if ($argType instanceof NullType) { - $thisType = null; - } else { - $thisType = $argType; - } + $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() || $methodReflection->getName() === '__construct') { + $nodeCallback(new InvalidateExprNode($expr->var), $scope); + $scope = $scope->invalidateExpression($expr->var, 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 { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + } + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } elseif ($expr instanceof Expr\NullsafeMethodCall) { + $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); + $exprResult = $this->processExprNode($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 = $scope->getType($expr->class)->getObjectClassNames(); + if (count($objectClasses) !== 1) { + $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); + } + if (count($objectClasses) === 1) { + $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($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; + } + $scope = $classResult->getScope(); + } + + $parametersAcceptor = null; + $methodReflection = null; + if ($expr->name instanceof Expr) { + $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) { + $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; + } + + $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; } - $scopeClass = 'static'; - if (isset($expr->args[2])) { - $argValue = $expr->args[2]->value; - $argValueType = $scope->getType($argValue); - - $directClassNames = TypeUtils::getDirectClassNames($argValueType); - if (count($directClassNames) === 1) { - $scopeClass = $directClassNames[0]; - } elseif ( - $argValue instanceof Expr\ClassConstFetch - && $argValue->name instanceof Node\Identifier - && strtolower($argValue->name->name) === 'class' - && $argValue->class instanceof Name - ) { - $scopeClass = $scope->resolveName($argValue->class); - } elseif ($argValueType instanceof ConstantStringType) { - $scopeClass = $argValueType->getValue(); + + $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value); + if ($nativeArgType->isNull()->yes()) { + $nativeThisType = null; + } else { + $nativeThisType = $nativeArgType; + } + } + $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); } + $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } + } else { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->args, $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->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 + && !$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, $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; - } elseif ($expr instanceof Expr\ArrowFunction) { - foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); - } - if ($expr->returnType !== null) { - $nodeCallback($expr->returnType, $scope); - } - - $arrowFunctionScope = $scope->enterArrowFunction($expr); - $nodeCallback(new InArrowFunctionNode($expr), $arrowFunctionScope); - $this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, $context); - $hasYield = false; + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + return new ExpressionResult( + $processClosureResult->getScope(), + false, + [], + [], + ); + } elseif ($expr instanceof Expr\ArrowFunction) { + $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); - $result = $this->processExprNode($arrayItem, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $scope = $result->getScope(); + $nodeCallback($arrayItem, $scope); + if ($arrayItem->key !== null) { + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $keyResult->hasYield(); + $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $scope = $keyResult->getScope(); + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $scope = $valueResult->getScope(); } $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); - } elseif ($expr instanceof ArrayItem) { - $hasYield = false; - if ($expr->key !== null) { - $result = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $scope = $result->getScope(); - } - $result = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $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()); + } + + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $context); return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), - static function () use ($expr, $rightResult): MutatingScope { - return $rightResult->getScope()->filterByTruthyValue($expr); - }, - static function () use ($leftMergedWithRightScope, $expr): MutatingScope { - return $leftMergedWithRightScope->filterByFalseyValue($expr); - } + 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()); + } + + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $context); return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), - static function () use ($leftMergedWithRightScope, $expr): MutatingScope { - return $leftMergedWithRightScope->filterByTruthyValue($expr); - }, - static function () use ($expr, $rightResult): MutatingScope { - return $rightResult->getScope()->filterByFalseyValue($expr); - } + 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) { - $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(); - $scope = $result->getScope(); - $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - if (!$expr->left instanceof PropertyFetch) { - $scope = $this->lookForExitVariableAssign($scope, $expr->left); - } - $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope()->mergeWith($scope); - $hasYield = $hasYield || $result->hasYield(); + $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(); - $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $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($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\Eval_ - || $expr instanceof Expr\Include_ - || $expr instanceof Expr\Print_ || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus - || $expr instanceof Expr\YieldFrom ) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - if ($expr instanceof Expr\YieldFrom) { - $hasYield = true; - } else { - $hasYield = $result->hasYield(); - } + $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($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($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; 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; + } + foreach (array_reverse($expr->vars) as $var) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); + } + foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) { $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - $scope = $this->lookForExitVariableAssign($scope, $var); } } 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; - if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - } 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)) { + $throwPoints = []; + $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 { + $className = $scope->resolveName($expr->class); + } + + $classReflection = null; + if ($className !== null && $this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $expr->args, - $constructorReflection->getVariants() + $expr->getArgs(), + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); + $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope); + if ($constructorThrowPoint !== null) { + $throwPoints[] = $constructorThrowPoint; + } + } + } else { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + } + + if ($constructorReflection !== null) { + if (!$constructorReflection->hasSideEffects()->no()) { + $certain = $constructorReflection->isPure()->no(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + sprintf('instantiation of class %s', $constructorReflection->getDeclaringClass()->getDisplayName()), + $certain, ); } + } elseif ($classReflection === null) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + 'instantiation of unknown class', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + } + + } else { + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name + $constructorResult = null; + $this->processStmtNode($expr->class, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { + $nodeCallback($node, $scope); + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + if ($constructorResult !== null) { + return; + } + $currentClassReflection = $node->getClassReflection(); + if ($currentClassReflection->getName() !== $classReflection->getName()) { + return; + } + if (!$currentClassReflection->hasConstructor()) { + return; + } + if ($currentClassReflection->getConstructor()->getName() !== $node->getMethodReflection()->getName()) { + return; + } + $constructorResult = $node; + }, StatementContext::createTopLevel()); + if ($constructorResult !== null) { + $throwPoints = array_merge($throwPoints, $constructorResult->getStatementResult()->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $constructorResult->getImpurePoints()); + } + if ($classReflection->hasConstructor()) { + $constructorReflection = $classReflection->getConstructor(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); } } - $result = $this->processArgs($constructorReflection, $parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); + + $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($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); - if ( - $expr->var instanceof Variable - || $expr->var instanceof ArrayDimFetch - || $expr->var instanceof PropertyFetch - || $expr->var instanceof StaticPropertyFetch - ) { - $newExpr = $expr; - if ($expr instanceof Expr\PostInc) { - $newExpr = new Expr\PreInc($expr->var); - } elseif ($expr instanceof Expr\PostDec) { - $newExpr = new Expr\PreDec($expr->var); - } + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); - if (!$scope->getType($expr->var)->equals($scope->getType($newExpr))) { - $scope = $this->processAssignVar( - $scope, - $expr->var, - $newExpr, - static function (): void { - }, - $context, - static function (MutatingScope $scope): ExpressionResult { - return new ExpressionResult($scope, false); - }, - false - )->getScope(); - } else { - $scope = $scope->invalidateExpression($expr->var); - } + $newExpr = $expr; + if ($expr instanceof Expr\PostInc) { + $newExpr = new Expr\PreInc($expr->var); + } elseif ($expr instanceof Expr\PostDec) { + $newExpr = new Expr\PreDec($expr->var); } + + $scope = $this->processAssignVar( + $scope, + $stmt, + $expr->var, + $newExpr, + 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, [], []), + 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) { - $ifTrueScope = $this->processExprNode($expr->if, $ifTrueScope, $nodeCallback, $context)->getScope(); - $ifFalseScope = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context)->getScope(); + $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($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); + $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); + $ifFalseScope = $elseResult->getScope(); + + $condType = $scope->getType($expr->cond); + if ($condType->isTrue()->yes()) { + $finalScope = $ifTrueScope; + } elseif ($condType->isFalse()->yes()) { + $finalScope = $ifFalseScope; } else { - $ifFalseScope = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context)->getScope(); - } + if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { + $finalScope = $ifFalseScope; + } else { + $ifFalseType = $ifFalseScope->getType($expr->else); - $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $finalScope = $ifTrueScope; + } else { + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + } + } + } return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), - static function () use ($finalScope, $expr): MutatingScope { - return $finalScope->filterByTruthyValue($expr); - }, - static function () use ($finalScope, $expr): MutatingScope { - return $finalScope->filterByFalseyValue($expr); - } + $throwPoints, + $impurePoints, + static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), + static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); } elseif ($expr instanceof Expr\Yield_) { + $throwPoints = [ + ThrowPoint::createImplicit($scope, $expr), + ]; + $impurePoints = [ + new ImpurePoint( + $scope, + $expr, + 'yield', + 'yield', + true, + ), + ]; if ($expr->key !== null) { - $scope = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $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) { - $scope = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $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; - } else { - $hasYield = false; - } - - return new ExpressionResult( - $scope, - $hasYield, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } - ); - } + } elseif ($expr instanceof Expr\Match_) { + $deepContext = $context->enterDeep(); + $condType = $scope->getType($expr->cond); + $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext); + $scope = $condResult->getScope(); + $hasYield = $condResult->hasYield(); + $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $matchScope = $scope->enterMatch($expr); + $armNodes = []; + $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; + } - /** - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param Expr $expr - * @param MutatingScope $scope - * @param ExpressionContext $context - */ + $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) { + $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); + continue; + } + + if (count($arm->conds) === 0) { + throw new ShouldNotHappenException(); + } + + $filteringExprs = []; + $armCondScope = $matchScope; + $condNodes = []; + foreach ($arm->conds as $armCond) { + $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); + $armCondResultScope = $armCondResult->getScope(); + $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); + if ($armCondType->isTrue()->yes()) { + $hasAlwaysTrueCond = true; + } + $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr); + $filteringExprs[] = $armCond; + } + + $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, + $bodyScope, + $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()); + $matchScope = $matchScope->filterByFalseyValue($filteringExpr); + } + + $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($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($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($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($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($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($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($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, + FuncCall $funcCall, + MutatingScope $scope, + ): ?ThrowPoint + { + $normalizedFuncCall = $funcCall; + if ($parametersAcceptor !== null) { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $funcCall); + } + + 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); + } + } + + $throwType = $functionReflection->getThrowType(); + if ($throwType === null && $parametersAcceptor !== null) { + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + return ThrowPoint::createExplicit($scope, $throwType, $funcCall, true); + } + } elseif ($this->implicitThrows) { + $requiredParameters = null; + if ($parametersAcceptor !== null) { + $requiredParameters = 0; + foreach ($parametersAcceptor->getParameters() as $parameter) { + if ($parameter->isOptional()) { + continue; + } + + $requiredParameters++; + } + } + if ( + !$functionReflection->isBuiltin() + || $requiredParameters === null + || $requiredParameters > 0 + || count($funcCall->getArgs()) > 0 + ) { + $functionReturnedType = $scope->getType($funcCall); + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { + return ThrowPoint::createImplicit($scope, $funcCall); + } + } + } + + return null; + } + + private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint + { + $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { + if (!$extension->isMethodSupported($methodReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } + + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + } + } + + $throwType = $methodReflection->getThrowType(); + if ($throwType === null) { + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); + } + } elseif ($this->implicitThrows) { + $methodReturnedType = $scope->getType($methodCall); + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + return ThrowPoint::createImplicit($scope, $methodCall); + } + } + + return null; + } + + /** + * @param Node\Arg[] $args + */ + 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); + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($constructorReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } + + return ThrowPoint::createExplicit($scope, $throwType, $new, false); + } + } + + if ($constructorReflection->getThrowType() !== null) { + $throwType = $constructorReflection->getThrowType(); + if (!$throwType->isVoid()->yes()) { + return ThrowPoint::createExplicit($scope, $throwType, $new, true); + } + } elseif ($this->implicitThrows) { + if (!$classReflection->is(Throwable::class)) { + return ThrowPoint::createImplicit($scope, $methodCall); + } + } + + return null; + } + + private function getStaticMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint + { + $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); + if ($normalizedMethodCall !== null) { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($methodReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } + + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); + } + } + + if ($methodReflection->getThrowType() !== null) { + $throwType = $methodReflection->getThrowType(); + if (!$throwType->isVoid()->yes()) { + return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); + } + } elseif ($this->implicitThrows) { + $methodReturnedType = $scope->getType($methodCall); + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + return ThrowPoint::createImplicit($scope, $methodCall); + } + } + + 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[] + */ + private function getAssignedVariables(Expr $expr): array + { + if ($expr instanceof Expr\Variable) { + if (is_string($expr->name)) { + return [$expr->name]; + } + + return []; + } + + if ($expr instanceof Expr\List_) { + $names = []; + foreach ($expr->items as $item) { + if ($item === null) { + continue; + } + + $names = array_merge($names, $this->getAssignedVariables($item->value)); + } + + return $names; + } + + if ($expr instanceof ArrayDimFetch) { + return $this->getAssignedVariables($expr->var); + } + + return []; + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ private function callNodeCallbackWithExpression( - \Closure $nodeCallback, + callable $nodeCallback, Expr $expr, MutatingScope $scope, - ExpressionContext $context + ExpressionContext $context, ): void { if ($context->isDeep()) { @@ -1978,36 +4458,30 @@ private function callNodeCallbackWithExpression( } /** - * @param \PhpParser\Node\Expr\Closure $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param Type|null $passedToType - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processClosureNode( + Node\Stmt $stmt, Expr\Closure $expr, MutatingScope $scope, - \Closure $nodeCallback, + callable $nodeCallback, ExpressionContext $context, - ?Type $passedToType - ): ExpressionResult + ?Type $passedToType, + ): ProcessClosureResult { foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } $byRefUses = []; - if ($passedToType !== null && !$passedToType->isCallable()->no()) { - $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) { @@ -2017,9 +4491,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; @@ -2031,10 +4507,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; } @@ -2048,112 +4534,277 @@ 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 = []; - $closureStmtsCallback = static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements): void { + $gatheredYieldStatements = []; + $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; + } if (!$node instanceof Return_) { return; } $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, - $statementResult + $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; + } + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $closureScope = $prevScope->generalizeWith($closureScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + + 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 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, + ?Type $passedToType, + ): ExpressionResult + { + foreach ($expr->params as $param) { + $this->processParamNode($stmt, $param, $scope, $nodeCallback); + } + if ($expr->returnType !== null) { + $nodeCallback($expr->returnType, $scope); + } + + $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()); - $count = 0; - do { - $prevScope = $closureScope; + return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + } - $intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void { - }); - $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); - foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { - $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); - } - $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); - if ($closureScope->equals($prevScope)) { - break; + /** + * @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; } - $count++; - } while ($count < self::LOOP_SCOPE_ITERATIONS); - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); - $nodeCallback(new ClosureReturnStatementsNode( - $expr, - $gatheredReturnStatements, - $statementResult - ), $closureScope); + $acceptors = $closureType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); - return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false); - } + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($args[$index])) { + continue; + } - private function lookForArrayDestructuringArray(MutatingScope $scope, Expr $expr, Type $valueType): MutatingScope - { - if ($expr instanceof Array_ || $expr instanceof List_) { - foreach ($expr->items as $key => $item) { - /** @var \PhpParser\Node\Expr\ArrayItem|null $itemValue */ - $itemValue = $item; - if ($itemValue === null) { - continue; + $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(), + )); - $keyType = $itemValue->key === null ? new ConstantIntegerType($key) : $scope->getType($itemValue->key); - $scope = $this->specifyItemFromArrayDestructuring($scope, $itemValue, $valueType, $keyType); + if ($passedToType->isCallable()->no()) { + return null; + } } - } elseif ($expr instanceof Variable && is_string($expr->name)) { - $scope = $scope->assignVariable($expr->name, new MixedType()); - } elseif ($expr instanceof ArrayDimFetch && $expr->var instanceof Variable && is_string($expr->var->name)) { - $scope = $scope->assignVariable($expr->var->name, new MixedType()); - } - return $scope; - } + $acceptors = $passedToType->getCallableParametersAcceptors($scope); + 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; + } - private function specifyItemFromArrayDestructuring(MutatingScope $scope, ArrayItem $arrayItem, Type $valueType, Type $keyType): MutatingScope - { - $type = $valueType->getOffsetValueType($keyType); - - $itemNode = $arrayItem->value; - if ($itemNode instanceof Variable && is_string($itemNode->name)) { - $scope = $scope->assignVariable($itemNode->name, $type); - } elseif ($itemNode instanceof ArrayDimFetch && $itemNode->var instanceof Variable && is_string($itemNode->var->name)) { - $currentType = $scope->hasVariableType($itemNode->var->name)->no() - ? new ConstantArrayType([], []) - : $scope->getVariableType($itemNode->var->name); - $dimType = null; - if ($itemNode->dim !== null) { - $dimType = $scope->getType($itemNode->dim); - } - $scope = $scope->assignVariable($itemNode->var->name, $currentType->setOffsetValueType($dimType, $type)); - } else { - $scope = $this->lookForArrayDestructuringArray($scope, $itemNode, $type); + $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; + } + } } - return $scope; + return $callableParameters; } /** - * @param \PhpParser\Node\Param $param - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processParamNode( + Node\Stmt $stmt, Node\Param $param, MutatingScope $scope, - \Closure $nodeCallback + callable $nodeCallback, ): void { + $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $nodeCallback); + $nodeCallback($param, $scope); if ($param->type !== null) { $nodeCallback($param->type, $scope); } @@ -2161,168 +4812,677 @@ 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 \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection|null $calleeReflection - * @param ParametersAcceptor|null $parametersAcceptor - * @param \PhpParser\Node\Arg[] $args - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param \PHPStan\Analyser\MutatingScope|null $closureBindScope - * @return \PHPStan\Analyser\ExpressionResult + * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArgs( + Node\Stmt $stmt, $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, ?ParametersAcceptor $parametersAcceptor, - array $args, + CallLike $callLike, MutatingScope $scope, - \Closure $nodeCallback, + callable $nodeCallback, ExpressionContext $context, - ?MutatingScope $closureBindScope = null + ?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; } + } - if ($calleeReflection instanceof FunctionReflection) { - if ( - $i === 0 - && $calleeReflection->getName() === 'array_map' - && isset($args[1]) - ) { - $parameterType = new CallableType([ - new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ], new MixedType(), false); - } - - if ( - $i === 1 - && $calleeReflection->getName() === 'array_filter' - && isset($args[0]) - ) { - $parameterType = new CallableType([ - new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ], new MixedType(), false); - } + $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); + $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->processClosureNode($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(); + + 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); + $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; + } + + $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; + } + } + + if (count($paramOutTypes) === 1) { + return $paramOutTypes[0]; } - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); + if (count($paramOutTypes) > 1) { + return TypeCombinator::union(...$paramOutTypes); } - return new ExpressionResult($scope, $hasYield); + return null; } /** - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \PhpParser\Node\Expr $var - * @param \PhpParser\Node\Expr $assignedExpr - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param \Closure(MutatingScope $scope): ExpressionResult $processExprCallback - * @param bool $enterExpressionAssign - * @return ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback + * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback */ private function processAssignVar( MutatingScope $scope, + Node\Stmt $stmt, Expr $var, Expr $assignedExpr, - \Closure $nodeCallback, + callable $nodeCallback, ExpressionContext $context, - \Closure $processExprCallback, - bool $enterExpressionAssign + Closure $processExprCallback, + bool $enterExpressionAssign, ): ExpressionResult { $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); + + $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(); - $scope = $scope->assignVariable($var->name, $scope->getType($assignedExpr)); + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); + + $truthyType = TypeCombinator::removeFalsey($type); + $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); + + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + + $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) { - $dimExprStack[] = $var->dim; + 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, + ); + $dimFetchStack[] = $var; $var = $var->var; } // 1. eval root expr - if ($enterExpressionAssign && $var instanceof Variable) { + if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } - $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); - if ($enterExpressionAssign && $var instanceof Variable) { + if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); } // 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(); if ($enterExpressionAssign) { @@ -2332,257 +5492,1005 @@ 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); - if (!(new ObjectType(\ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { - // 4. compose types - if ($varType instanceof ErrorType) { - $varType = new ConstantArrayType([], []); - } - $offsetValueType = $varType; - $offsetValueTypeStack = [$offsetValueType]; - foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { - if ($offsetType === null) { - $offsetValueType = new ConstantArrayType([], []); + $varNativeType = $scope->getNativeType($var); - } else { - $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); - if ($offsetValueType instanceof ErrorType) { - $offsetValueType = new ConstantArrayType([], []); + // 4. compose types + if ($varType instanceof ErrorType) { + $varType = new ConstantArrayType([], []); + } + if ($varNativeType instanceof ErrorType) { + $varNativeType = new ConstantArrayType([], []); + } + $offsetValueType = $varType; + $offsetNativeValueType = $varNativeType; + + $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); - - /** @phpstan-ignore-next-line */ - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + if (!$rewritten) { + $nativeValueToWrite = $valueToWrite; } + } + 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 + $valueToWrite, + $nativeValueToWrite, ); } + + if ($originalVar->dim instanceof Variable || $originalVar->dim instanceof Node\Scalar) { + $currentVarType = $scope->getType($originalVar); + $currentVarNativeType = $scope->getNativeType($originalVar); + if ( + !$originalValueToWrite->isSuperTypeOf($currentVarType)->yes() + || !$originalNativeValueToWrite->isSuperTypeOf($currentVarNativeType)->yes() + ) { + $scope = $scope->assignExpression( + $originalVar, + $originalValueToWrite, + $originalNativeValueToWrite, + ); + } + } + } else { + if ($var instanceof Variable) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } + } + } + + if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { + $throwPoints = array_merge($throwPoints, $this->processExprNode( + $stmt, + new MethodCall($var, 'offsetSet'), + $scope, + static function (): void { + }, + $context, + )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { - $this->processExprNode($var->var, $scope, $nodeCallback, $context); - $result = $processExprCallback($scope); - $hasYield = $result->hasYield(); - $scope = $result->getScope(); + $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context); + $hasYield = $objectResult->hasYield(); + $throwPoints = $objectResult->getThrowPoints(); + $impurePoints = $objectResult->getImpurePoints(); + $scope = $objectResult->getScope(); - $propertyHolderType = $scope->getType($var->var); $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; + } else { + $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, $scope->getType($assignedExpr)); + 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)); + } + } + $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 - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $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 { + }, + $context, + )->getThrowPoints()); + } } } elseif ($var instanceof Expr\StaticPropertyFetch) { - if ($var->class instanceof \PhpParser\Node\Name) { - $propertyHolderType = new ObjectType($scope->resolveName($var->class)); + 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); } - $result = $processExprCallback($scope); - $hasYield = $result->hasYield(); - $scope = $result->getScope(); - $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; + } else { + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); + $hasYield = $propertyNameResult->hasYield(); + $throwPoints = $propertyNameResult->getThrowPoints(); + $impurePoints = $propertyNameResult->getImpurePoints(); + $scope = $propertyNameResult->getScope(); } - if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) { - $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope); - if ($propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + + $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) { + $propertyReflection = $scope->getPropertyReflection($propertyHolderType, $propertyName); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { + 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 - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } + } 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) { + continue; + } + + $itemScope = $scope; + if ($enterExpressionAssign) { + $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); + } + $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\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, [], []), + $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 $valueToWrite; + } + + private function unwrapAssign(Expr $expr): Expr + { + if ($expr instanceof Assign) { + return $this->unwrapAssign($expr->expr); + } + + return $expr; + } + + /** + * @param array $conditionalExpressions + * @return array + */ + private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array + { + foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) { + if (!$expr instanceof Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + if ($expr->name === $variableName) { + continue; + } + + if (!isset($conditionalExpressions[$exprString])) { + $conditionalExpressions[$exprString] = []; + } + + $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; + } + + /** + * @param array $conditionalExpressions + * @return array + */ + private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array + { + foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) { + if (!$expr instanceof Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + if ($expr->name === $variableName) { + continue; } + + if (!isset($conditionalExpressions[$exprString])) { + $conditionalExpressions[$exprString] = []; + } + + $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 new ExpressionResult($scope, $hasYield); + return $conditionalExpressions; } - private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr): MutatingScope - { - $comment = CommentHelper::getDocComment($stmt); - if ($comment === null) { - return $scope; - } + /** + * @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 = []; + + foreach ($stmt->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + + $assignedVariable = null; + if ( + $stmt instanceof Node\Stmt\Expression + && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef) + && $stmt->expr->var instanceof Variable + && is_string($stmt->expr->var->name) + ) { + $assignedVariable = $stmt->expr->var->name; + } + + foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { + if (is_int($name)) { + $variableLessTags[] = $varTag; + continue; + } + + if ($name === $assignedVariable) { + continue; + } + + $certainty = $scope->hasVariableType($name); + if ($certainty->no()) { + continue; + } - $function = $scope->getFunction(); + if ($scope->isInClass() && $scope->getFunction() === null) { + continue; + } - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment - ); + if ($scope->canAnyVariableExist()) { + $certainty = TrinaryLogic::createYes(); + } - $variableLessTags = []; - foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { - if (is_int($name)) { - $variableLessTags[] = $varTag; - continue; - } + $variableNode = new Variable($name, $stmt->getAttributes()); + $originalType = $scope->getVariableType($name); + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope); + } - $certainty = $scope->hasVariableType($name); - if ($certainty->no()) { - continue; + $scope = $scope->assignVariable( + $name, + $varTag->getType(), + $scope->getNativeType($variableNode), + $certainty, + ); } - - $scope = $scope->assignVariable($name, $varTag->getType(), $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; } - private function processVarAnnotation(MutatingScope $scope, string $variableName, string $comment, bool $strict, bool &$changed = false): MutatingScope + /** + * @param array $variableNames + */ + private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope { $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, - $comment - ); - $varTags = $resolvedPhpDoc->getVarTags(); + $varTags = []; + foreach ($node->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { + $varTags[$key] = $varTag; + } + } + + if (count($varTags) === 0) { + return $scope; + } + + foreach ($variableNames as $variableName) { + if (!isset($varTags[$variableName])) { + continue; + } - if (isset($varTags[$variableName])) { $variableType = $varTags[$variableName]->getType(); $changed = true; - return $scope->assignVariable($variableName, $variableType); - + $scope = $scope->assignVariable($variableName, $variableType, new MixedType(), TrinaryLogic::createYes()); } - if (!$strict && count($varTags) === 1 && isset($varTags[0])) { + if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { $variableType = $varTags[0]->getType(); $changed = true; - return $scope->assignVariable($variableName, $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 { - $comment = CommentHelper::getDocComment($stmt); - if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) { + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { + $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); + } + $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 instanceof Variable && is_string($stmt->keyVar->name)) { + $keyVarName = $stmt->keyVar->name; + } $scope = $scope->enterForeach( + $originalScope, $stmt->expr, $stmt->valueVar->name, - $stmt->keyVar !== null - && $stmt->keyVar instanceof Variable - && is_string($stmt->keyVar->name) - ? $stmt->keyVar->name - : null + $keyVarName, ); - if ($comment !== null) { - $scope = $this->processVarAnnotation($scope, $stmt->valueVar->name, $comment, true); + $vars = [$stmt->valueVar->name]; + if ($keyVarName !== null) { + $vars[] = $keyVarName; + } + } 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, [], []), + true, + )->getScope(); + $vars = $this->getAssignedVariables($stmt->valueVar); + if ( + $stmt->keyVar instanceof Variable && is_string($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->keyVar instanceof Variable && is_string($stmt->keyVar->name) + $stmt->getDocComment() === null + && $iterateeType->isConstantArray()->yes() + && count($constantArrays) === 1 + && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) + && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name); - - if ($comment !== null) { - $scope = $this->processVarAnnotation($scope, $stmt->keyVar->name, $comment, true); - } - } - - if ($stmt->valueVar instanceof List_ || $stmt->valueVar instanceof Array_) { - $exprType = $scope->getType($stmt->expr); - $itemType = $exprType->getIterableValueType(); - $scope = $this->lookForArrayDestructuringArray($scope, $stmt->valueVar, $itemType); - $comment = CommentHelper::getDocComment($stmt); - if ($comment !== null) { - foreach ($stmt->valueVar->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } - if (!$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) { - continue; - } - - $scope = $this->processVarAnnotation($scope, $arrayItem->value->name, $comment, true); - } + $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, + $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 $scope; + return $this->processVarAnnotation($scope, $vars, $stmt); } /** - * @param \PhpParser\Node\Stmt\TraitUse $node - * @param MutatingScope $classScope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, \Closure $nodeCallback): 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; } $traitReflection = $this->reflectionProvider->getClass($traitName); $traitFileName = $traitReflection->getFileName(); - if ($traitFileName === false) { + if ($traitFileName === null) { continue; // trait from eval or from PHP itself } $fileName = $this->fileHelper->normalizePath($traitFileName); 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, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $adaptations, $nodeCallback); + } + } + + /** + * @param Node[]|Node|scalar|null $node + * @param Node\Stmt\TraitUseAdaptation[] $adaptations + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, array $adaptations, callable $nodeCallback): void + { + if ($node instanceof Node) { + if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) { + $methodModifiers = []; + $methodNames = []; + foreach ($adaptations as $adaptation) { + if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + continue; + } + + $methodName = $adaptation->method->toLowerString(); + if ($adaptation->newModifier !== null) { + $methodModifiers[$methodName] = $adaptation->newModifier; + } + + if ($adaptation->newName === null) { + continue; + } + + $methodNames[$methodName] = $adaptation->newName; + } + + $stmts = $node->stmts; + foreach ($stmts as $i => $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + $methodName = $stmt->name->toLowerString(); + $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->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString()); + $methodAst->name = $methodNames[$methodName]; + } + + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $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) { + return; + } + if ($node instanceof Node\FunctionLike) { + return; + } + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->{$subNodeName}; + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback); + } + } elseif (is_array($node)) { + foreach ($node as $subNode) { + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback); + } + } + } + + 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 \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param ClassReflection $traitReflection - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param Node[]|Node|scalar|null $node + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, \Closure $nodeCallback): void + private function processNodesForCalledMethod($node, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void { if ($node instanceof Node) { - if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName) { - $this->processStmtNodes($node, $node->stmts, $scope->enterTrait($traitReflection), $nodeCallback); + $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) { @@ -2593,32 +6501,38 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } foreach ($node->getSubNodeNames() as $subNodeName) { $subNode = $node->{$subNodeName}; - $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $nodeCallback); + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); } } elseif (is_array($node)) { foreach ($node as $subNode) { - $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $nodeCallback); + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); } } } /** - * @param Scope $scope - * @param Node\FunctionLike $functionLike - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool} + * @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; - $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(); @@ -2626,29 +6540,72 @@ 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 \PHPStan\ShouldNotHappenException(); + 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $param->var->name; - }, $functionLike->getParams()); + }, $node->getParams()); $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $docComment, $file, $scope->getClassReflection(), $trait, - $functionLike->name->name, - $positionalParameterNames + $node->name->name, + $positionalParameterNames, ); - } elseif ($functionLike instanceof Node\Stmt\Function_) { - $functionName = trim($scope->getNamespace() . '\\' . $functionLike->name->name, '\\'); + + if ($node->name->toLowerString() === '__construct') { + foreach ($node->params as $param) { + if ($param->flags === 0) { + continue; + } + + if ($param->getDocComment() === null) { + continue; + } + + if ( + !$param->var instanceof Variable + || !is_string($param->var->name) + ) { + throw new ShouldNotHappenException(); + } + + $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $file, + $class, + $trait, + '__construct', + $param->getDocComment()->getText(), + ); + $varTags = $paramPhpDoc->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $phpDocType = $varTags[0]->getType(); + } elseif (isset($varTags[$param->var->name])) { + $phpDocType = $varTags[$param->var->name]->getType(); + } else { + continue; + } + + $phpDocParameterTypes[$param->var->name] = $phpDocType; + } + } + } 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) { @@ -2657,25 +6614,78 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $class, $trait, $functionName, - $docComment + $docComment, ); } + $varTags = []; if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTypes = array_map(static function (ParamTag $tag): Type { - return $tag->getType(); - }, $resolvedPhpDoc->getParamTags()); - $nativeReturnType = $scope->getFunctionType($functionLike->getReturnType(), false, false); - $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); + $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); + foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { + if (array_key_exists($paramName, $phpDocParameterTypes)) { + continue; + } + $paramType = $paramTag->getType(); + if ($scope->isInClass()) { + $paramType = $this->transformStaticType($scope->getClassReflection(), $paramType); + } + $phpDocParameterTypes[$paramName] = $paramType; + } + foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) { + if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) { + continue; + } + $paramClosureThisType = $paramClosureThisTag->getType(); + if ($scope->isInClass()) { + $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType); + } + $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType; + } + + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); + } + if ($node instanceof Node\FunctionLike) { + $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false); + $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); + 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; $isDeprecated = $resolvedPhpDoc->isDeprecated(); $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]; + 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 + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type { + if ($type instanceof StaticType) { + $changedType = $type->changeBaseClass($declaringClass); + if ($declaringClass->isFinal() && !$type instanceof ThisType) { + $changedType = $changedType->getStaticObjectType(); + } + return $traverse($changedType); + } + + return $traverse($type); + }); } private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type @@ -2699,4 +6709,79 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $ return null; } + /** + * @param list $expressions + */ + private function createBooleanAndFromExpressions(array $expressions): ?Expr + { + if (count($expressions) === 0) { + return null; + } + + if (count($expressions) === 1) { + return $expressions[0]; + } + + $left = array_shift($expressions); + $right = $this->createBooleanAndFromExpressions($expressions); + + if ($right === null) { + throw new ShouldNotHappenException(); + } + + return new BooleanAnd($left, $right); + } + + /** + * @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 new file mode 100644 index 0000000000..0b439191c7 --- /dev/null +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -0,0 +1,83 @@ +getType($expr))) { + // We're in most likely in context of a null-safe operator ($scope->moreSpecificType is defined for $expr) + // Modifying the expression would not bring any value or worse ruin the context information + return $expr; + } + + return self::getNullsafeShortcircuitedExpr($expr); + } + + /** + * @internal Use NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope + */ + public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr + { + if ($expr instanceof Expr\NullsafeMethodCall) { + return new Expr\MethodCall(self::getNullsafeShortcircuitedExpr($expr->var), $expr->name, $expr->args); + } + + if ($expr instanceof Expr\MethodCall) { + $var = self::getNullsafeShortcircuitedExpr($expr->var); + if ($expr->var === $var) { + return $expr; + } + + return new Expr\MethodCall($var, $expr->name, $expr->getArgs()); + } + + if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { + $class = self::getNullsafeShortcircuitedExpr($expr->class); + if ($expr->class === $class) { + return $expr; + } + + return new Expr\StaticCall($class, $expr->name, $expr->getArgs()); + } + + if ($expr instanceof Expr\ArrayDimFetch) { + $var = self::getNullsafeShortcircuitedExpr($expr->var); + if ($expr->var === $var) { + return $expr; + } + + return new Expr\ArrayDimFetch($var, $expr->dim); + } + + if ($expr instanceof Expr\NullsafePropertyFetch) { + return new Expr\PropertyFetch(self::getNullsafeShortcircuitedExpr($expr->var), $expr->name); + } + + if ($expr instanceof Expr\PropertyFetch) { + $var = self::getNullsafeShortcircuitedExpr($expr->var); + if ($expr->var === $var) { + return $expr; + } + + return new Expr\PropertyFetch($var, $expr->name); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + $class = self::getNullsafeShortcircuitedExpr($expr->class); + if ($expr->class === $class) { + return $expr; + } + + return new Expr\StaticPropertyFetch($class, $expr->name); + } + + return $expr; + } + +} diff --git a/src/Analyser/OutOfClassScope.php b/src/Analyser/OutOfClassScope.php index 538c9ba914..a2215bc25c 100644 --- a/src/Analyser/OutOfClassScope.php +++ b/src/Analyser/OutOfClassScope.php @@ -2,15 +2,21 @@ 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 */ + public function __construct() + { + } + public function isInClass(): bool { return false; @@ -26,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 1c42abe712..1708a1f53b 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -3,43 +3,44 @@ namespace PHPStan\Analyser\ResultCache; use PHPStan\Analyser\Error; +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 { - private bool $fullAnalysis; - - /** @var string[] */ - private array $filesToAnalyse; - - private int $lastFullAnalysisTime; - - /** @var array> */ - private array $errors; - - /** @var array> */ - private array $dependencies; - /** * @param string[] $filesToAnalyse - * @param bool $fullAnalysis - * @param int $lastFullAnalysisTime - * @param array> $errors + * @param mixed[] $meta + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param CollectorData $collectedData * @param array> $dependencies + * @param array> $exportedNodes + * @param array $projectExtensionFiles */ public function __construct( - array $filesToAnalyse, - bool $fullAnalysis, - int $lastFullAnalysisTime, - array $errors, - array $dependencies + private array $filesToAnalyse, + private bool $fullAnalysis, + 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, ) { - $this->filesToAnalyse = $filesToAnalyse; - $this->fullAnalysis = $fullAnalysis; - $this->lastFullAnalysisTime = $lastFullAnalysisTime; - $this->errors = $errors; - $this->dependencies = $dependencies; } /** @@ -61,13 +62,53 @@ public function getLastFullAnalysisTime(): int } /** - * @return array> + * @return mixed[] + */ + public function getMeta(): array + { + return $this->meta; + } + + /** + * @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> */ @@ -76,4 +117,20 @@ public function getDependencies(): array return $this->dependencies; } + /** + * @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 new file mode 100644 index 0000000000..75f3d25628 --- /dev/null +++ b/src/Analyser/ResultCache/ResultCacheClearer.php @@ -0,0 +1,28 @@ +cacheFilePath); + if (!is_file($this->cacheFilePath)) { + return $dir; + } + + @unlink($this->cacheFilePath); + + return $dir; + } + +} diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 988dba2ea3..e2ad34e817 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,100 +2,186 @@ namespace PHPStan\Analyser\ResultCache; +use Nette\Neon\Neon; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; -use PHPStan\File\FileReader; +use PHPStan\Analyser\FileAnalyserResult; +use PHPStan\Collectors\CollectedData; +use PHPStan\Command\Output; +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\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; +use function array_diff; use function array_fill_keys; +use function array_filter; use function array_key_exists; - -class ResultCacheManager +use function array_keys; +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 ksort; +use function microtime; +use function sha1_file; +use function sort; +use function sprintf; +use function str_starts_with; +use function time; +use function unlink; +use function var_export; +use const PHP_VERSION_ID; + +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + * @phpstan-import-type CollectorData from CollectedData + */ +final class ResultCacheManager { - private const CACHE_VERSION = 'v4-callback'; - - private string $cacheFilePath; - - /** @var string[] */ - private array $allCustomConfigFiles; - - /** @var string[] */ - private array $analysedPaths; - - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - - /** @var string[] */ - private array $stubFiles; - - private string $usedLevel; - - private ?string $cliAutoloadFile; + private const CACHE_VERSION = 'v12-linesToIgnore'; /** @var array */ private array $fileHashes = []; + /** @var array */ + private array $alreadyProcessed = []; + /** - * @param string $cacheFilePath - * @param string[] $allCustomConfigFiles * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths - * @param string[] $stubFiles - * @param string $usedLevel - * @param string|null $cliAutoloadFile + * @param string[] $bootstrapFiles + * @param string[] $scanFiles + * @param string[] $scanDirectories + * @param list> $parametersNotInvalidatingCache */ public function __construct( - string $cacheFilePath, - array $allCustomConfigFiles, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $stubFiles, - string $usedLevel, - ?string $cliAutoloadFile + private Container $container, + private ExportedNodeFetcher $exportedNodeFetcher, + private FileFinder $scanFileFinder, + private ReflectionProvider $reflectionProvider, + private StubFilesProvider $stubFilesProvider, + private FileHelper $fileHelper, + private string $cacheFilePath, + private array $analysedPaths, + private array $composerAutoloaderProjectPaths, + private string $usedLevel, + private ?string $cliAutoloadFile, + private array $bootstrapFiles, + private array $scanFiles, + private array $scanDirectories, + private bool $checkDependenciesOfProjectExtensionFiles, + private array $parametersNotInvalidatingCache, + private int $skipResultCacheIfOlderThanDays, ) { - $this->cacheFilePath = $cacheFilePath; - $this->allCustomConfigFiles = $allCustomConfigFiles; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->stubFiles = $stubFiles; - $this->usedLevel = $usedLevel; - $this->cliAutoloadFile = $cliAutoloadFile; } /** * @param string[] $allAnalysedFiles - * @param bool $debug - * @return ResultCache + * @param mixed[]|null $projectConfigArray */ - public function restore(array $allAnalysedFiles, bool $debug): ResultCache + public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output): ResultCache { + $startTime = microtime(true); if ($debug) { - return new ResultCache($allAnalysedFiles, true, time(), [], []); + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache not used because of debug mode.'); + } + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); + } + if ($onlyFiles) { + 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), [], [], [], [], [], [], [], []); } - if (!is_file($this->cacheFilePath)) { - return new ResultCache($allAnalysedFiles, true, time(), [], []); + $cacheFilePath = $this->cacheFilePath; + if (!is_file($cacheFilePath)) { + 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), [], [], [], [], [], [], [], []); } try { - $data = require $this->cacheFilePath; - } catch (\Throwable $e) { - return new ResultCache($allAnalysedFiles, true, time(), [], []); + $data = require $cacheFilePath; + } catch (Throwable $e) { + 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), [], [], [], [], [], [], [], []); } if (!is_array($data)) { - @unlink($this->cacheFilePath); - return new ResultCache($allAnalysedFiles, true, time(), [], []); + @unlink($cacheFilePath); + 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), [], [], [], [], [], [], [], []); } - if ($data['meta'] !== $this->getMeta()) { - return new ResultCache($allAnalysedFiles, true, time(), [], []); + $meta = $this->getMeta($allAnalysedFiles, $projectConfigArray); + if ($this->isMetaDifferent($data['meta'], $meta)) { + 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, [], [], [], [], [], [], [], []); } - if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * 7) { - // run full analysis if the result cache is older than 7 days - return new ResultCache($allAnalysedFiles, true, time(), [], []); + $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 X days + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); + } + + /** + * @var string $fileHash + * @var bool $isAnalysed + */ + foreach ($data['projectExtensionFiles'] as $extensionFile => [$fileHash, $isAnalysed]) { + if (!$isAnalysed) { + continue; + } + if (!is_file($extensionFile)) { + 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, [], [], [], [], [], [], [], []); + } + + if ($this->getFileHash($extensionFile) === $fileHash) { + continue; + } + + 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, [], [], [], [], [], [], [], []); } $invertedDependencies = $data['dependencies']; @@ -103,12 +189,46 @@ public function restore(array $allAnalysedFiles, bool $debug): ResultCache $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->getStubFiles() as $stubFile) { + if (!array_key_exists($stubFile, $errors)) { + continue; + } + + $filteredErrors[$stubFile] = $errors[$stubFile]; + } + foreach ($allAnalysedFiles as $analysedFile) { if (array_key_exists($analysedFile, $errors)) { $filteredErrors[$analysedFile] = $errors[$analysedFile]; } + 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]; + } if (!array_key_exists($analysedFile, $invertedDependencies)) { // new file $filesToAnalyse[] = $analysedFile; @@ -129,6 +249,20 @@ public function restore(array $allAnalysedFiles, bool $debug): ResultCache } $filesToAnalyse[] = $analysedFile; + if (!array_key_exists($analysedFile, $filteredExportedNodes)) { + continue; + } + + $cachedFileExportedNodes = $filteredExportedNodes[$analysedFile]; + $exportedNodesChanged = $this->exportedNodesChanged($analysedFile, $cachedFileExportedNodes); + if ($exportedNodesChanged === null) { + continue; + } + + if ($exportedNodesChanged) { + $newFileAppeared = true; + } + foreach ($dependentFiles as $dependentFile) { if (!is_file($dependentFile)) { continue; @@ -158,10 +292,110 @@ public function restore(array $allAnalysedFiles, bool $debug): ResultCache } } - return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $filteredErrors, $invertedDependenciesToReturn); + $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']); + } + + /** + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + */ + private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool + { + $projectConfig = $currentMeta['projectConfig']; + if ($projectConfig !== null) { + ksort($currentMeta['projectConfig']); + + $currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']); + } + + return $cachedMeta !== $currentMeta; + } + + /** + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + * + * @return string[] + */ + private function getMetaKeyDifferences(array $cachedMeta, array $currentMeta): array + { + $diffs = []; + foreach ($cachedMeta as $key => $value) { + if (!array_key_exists($key, $currentMeta)) { + $diffs[] = $key; + continue; + } + + if ($value === $currentMeta[$key]) { + continue; + } + + $diffs[] = $key; + } + + if ($diffs === []) { + // when none of the keys is different, + // the order of the keys is the problem + $diffs[] = 'keyOrder'; + } + + return $diffs; + } + + /** + * @param array $cachedFileExportedNodes + * @return bool|null null means nothing changed, true means new root symbol appeared, false means nested node changed + */ + private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): ?bool + { + $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; + } + + foreach ($fileExportedNodes as $i => $fileExportedNode) { + $cachedExportedNode = $cachedFileExportedNodes[$i]; + if (!$cachedExportedNode->equals($fileExportedNode)) { + return false; + } + } + + return null; } - public function process(AnalyserResult $analyserResult, ResultCache $resultCache): AnalyserResult + public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, bool $save): ResultCacheProcessResult { $internalErrors = $analyserResult->getInternalErrors(); $freshErrorsByFile = []; @@ -169,38 +403,111 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $freshErrorsByFile[$error->getFilePath()][] = $error; } - $save = function (array $errorsByFile, ?array $dependencies) use ($internalErrors, $resultCache): void { + $freshLocallyIgnoredErrorsByFile = []; + foreach ($analyserResult->getLocallyIgnoredErrors() as $error) { + $freshLocallyIgnoredErrorsByFile[$error->getFilePath()][] = $error; + } + + $freshCollectedDataByFile = $analyserResult->getCollectedData(); + + $meta = $resultCache->getMeta(); + $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->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because only files were passed as analysed paths.'); + } + return false; + } if ($dependencies === null) { - return; + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because of error in dependencies.'); + } + return false; } if (count($internalErrors) > 0) { - return; + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because of internal errors.'); + } + return false; } foreach ($errorsByFile as $errors) { foreach ($errors as $error) { - if ($error->canBeIgnored()) { + if (!$error->hasNonIgnorableException()) { continue; } - return; + if ($output->isVeryVerbose()) { + $output->writeLineFormatted(sprintf('Result cache was not saved because of non-ignorable exception: %s', $error->getMessage())); + } + + return false; } } - $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $dependencies); + $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles, $meta); + + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache is saved.'); + } + + return true; }; if ($resultCache->isFullAnalysis()) { - $save($freshErrorsByFile, $analyserResult->getDependencies()); + $saved = false; + if ($save !== false) { + $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->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because it was not requested.'); + } + } - return $analyserResult; + return new ResultCacheProcessResult($analyserResult, $saved); } $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) { + $projectExtensionFiles = []; + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } - $save($errorsByFile, $dependencies); + // 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 = []; foreach ($errorsByFile as $fileErrors) { @@ -209,18 +516,32 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - return new AnalyserResult( + $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, - $analyserResult->hasReachedInternalErrorsCountLimit() - ); + $exportedNodes, + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), $saved); } /** - * @param ResultCache $resultCache - * @param array> $freshErrorsByFile - * @return array> + * @param array> $freshErrorsByFile + * @return array> */ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile): array { @@ -237,7 +558,42 @@ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile) } /** - * @param ResultCache $resultCache + * @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 */ @@ -259,7 +615,7 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend foreach (array_keys($filesNoOneIsDependingOn) as $file) { if (array_key_exists($file, $cachedDependencies)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $cachedDependencies[$file] = []; @@ -279,14 +635,84 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend } /** - * @param int $lastFullAnalysisTime - * @param array> $errors + * @param array> $freshExportedNodes + * @return array> + */ + private function mergeExportedNodes(ResultCache $resultCache, array $freshExportedNodes): array + { + $newExportedNodes = $resultCache->getExportedNodes(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshExportedNodes)) { + unset($newExportedNodes[$file]); + continue; + } + + $newExportedNodes[$file] = $freshExportedNodes[$file]; + } + + return $newExportedNodes; + } + + /** + * @param array $freshLinesToIgnore + * @return array + */ + private function mergeLinesToIgnore(ResultCache $resultCache, array $freshLinesToIgnore): array + { + $newLinesToIgnore = $resultCache->getLinesToIgnore(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshLinesToIgnore)) { + unset($newLinesToIgnore[$file]); + continue; + } + + $newLinesToIgnore[$file] = $freshLinesToIgnore[$file]; + } + + return $newLinesToIgnore; + } + + /** + * @param array $freshUnmatchedLineIgnores + * @return array + */ + private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $freshUnmatchedLineIgnores): array + { + $newUnmatchedLineIgnores = $resultCache->getUnmatchedLineIgnores(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshUnmatchedLineIgnores)) { + unset($newUnmatchedLineIgnores[$file]); + continue; + } + + $newUnmatchedLineIgnores[$file] = $freshUnmatchedLineIgnores[$file]; + } + + return $newUnmatchedLineIgnores; + } + + /** + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param array>> $collectedData * @param array> $dependencies + * @param array> $exportedNodes + * @param array $projectExtensionFiles + * @param mixed[] $meta */ private function save( int $lastFullAnalysisTime, array $errors, - array $dependencies + array $locallyIgnoredErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, + array $collectedData, + array $dependencies, + array $exportedNodes, + array $projectExtensionFiles, + array $meta, ): void { $invertedDependencies = []; @@ -306,7 +732,7 @@ private function save( foreach (array_keys($filesNoOneIsDependingOn) as $file) { if (array_key_exists($file, $invertedDependencies)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!is_file($file)) { @@ -320,96 +746,227 @@ 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, - 'errorsCallback' => static function (): array { return %s; }, - 'dependencies' => %s, -]; -php; + $file = $this->cacheFilePath; FileWriter::write( - $this->cacheFilePath, - sprintf( - $template, - var_export($lastFullAnalysisTime, true), - var_export($this->getMeta(), true), - var_export($errors, true), - var_export($invertedDependencies, true) - ) + $file, + " " . 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 + */ + private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array + { + $this->alreadyProcessed = []; + $projectExtensionFiles = []; + if ($projectConfig !== null) { + $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 = ProjectConfigHelper::getServiceClassNames($projectConfig); + foreach ($classes as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + 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); + if (count($allServiceFiles) === 0) { + $normalizedFileName = $this->fileHelper->normalizePath($fileName); + foreach ($vendorDirs as $vendorDir) { + if (str_starts_with($normalizedFileName, $vendorDir)) { + continue 2; + } + } + $projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class]; + continue; + } + + foreach ($allServiceFiles as $serviceFile) { + if (array_key_exists($serviceFile, $projectExtensionFiles)) { + continue; + } + + $projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class]; + } + } + } + + return $projectExtensionFiles; + } + + /** + * @param array> $dependencies + * @return array + */ + private function getAllDependencies(string $fileName, array $dependencies): array + { + if (!array_key_exists($fileName, $dependencies)) { + return []; + } + + if (array_key_exists($fileName, $this->alreadyProcessed)) { + return []; + } + + $this->alreadyProcessed[$fileName] = true; + + $files = [$fileName]; + + if ($this->checkDependenciesOfProjectExtensionFiles) { + foreach ($dependencies[$fileName] as $fileDep) { + foreach ($this->getAllDependencies($fileDep, $dependencies) as $fileDep2) { + $files[] = $fileDep2; + } + } + } + + return $files; + } + + /** + * @param string[] $allAnalysedFiles + * @param mixed[]|null $projectConfigArray * @return mixed[] */ - private function getMeta(): array + private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): array { - $extensions = array_values(array_filter(get_loaded_extensions(), static function (string $extension): bool { - return $extension !== 'xdebug'; - })); + $extensions = array_values(array_filter(get_loaded_extensions(), static fn (string $extension): bool => $extension !== 'xdebug')); sort($extensions); + if ($projectConfigArray !== null) { + 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, - 'configFiles' => $this->getConfigFiles(), + 'projectConfig' => $projectConfigArray, 'analysedPaths' => $this->analysedPaths, + 'scannedFiles' => $this->getScannedFiles($allAnalysedFiles), 'composerLocks' => $this->getComposerLocks(), - 'cliAutoloadFile' => $this->cliAutoloadFile, + 'composerInstalled' => $this->getComposerInstalled(), + 'executedFilesHashes' => $this->getExecutedFileHashes(), 'phpExtensions' => $extensions, 'stubFiles' => $this->getStubFiles(), 'level' => $this->usedLevel, ]; } - /** - * @return array - */ - private function getConfigFiles(): array + private function getFileHash(string $path): string { - $configFiles = []; - foreach ($this->allCustomConfigFiles as $configFile) { - $configFiles[$configFile] = $this->getFileHash($configFile); + if (array_key_exists($path, $this->fileHashes)) { + return $this->fileHashes[$path]; + } + + $hash = sha1_file($path); + if ($hash === false) { + throw new CouldNotReadFileException($path); } + $this->fileHashes[$path] = $hash; - return $configFiles; + return $hash; } - private function getFileHash(string $path): string + /** + * @param string[] $allAnalysedFiles + * @return array + */ + private function getScannedFiles(array $allAnalysedFiles): array { - if (array_key_exists($path, $this->fileHashes)) { - return $this->fileHashes[$path]; + $scannedFiles = $this->scanFiles; + foreach ($this->scanFileFinder->findFiles($this->scanDirectories)->getFiles() as $file) { + $scannedFiles[] = $file; } - $contents = FileReader::read($path); - $contents = str_replace("\r\n", "\n", $contents); + $scannedFiles = array_unique($scannedFiles); - $hash = sha1($contents); - $this->fileHashes[$path] = $hash; + $hashes = []; + foreach (array_diff($scannedFiles, $allAnalysedFiles) as $file) { + $hashes[$file] = $this->getFileHash($file); + } - return $hash; + ksort($hashes); + + return $hashes; } - private function getPhpStanVersion(): string + /** + * @return array + */ + private function getExecutedFileHashes(): array { - try { - return \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { - return 'Version unknown'; + $hashes = []; + if ($this->cliAutoloadFile !== null) { + $hashes[$this->cliAutoloadFile] = $this->getFileHash($this->cliAutoloadFile); } + + foreach ($this->bootstrapFiles as $bootstrapFile) { + $hashes[$bootstrapFile] = $this->getFileHash($bootstrapFile); + } + + ksort($hashes); + + return $hashes; } /** @@ -430,29 +987,73 @@ private function getComposerLocks(): array return $locks; } + /** + * @return array + */ + private function getComposerInstalled(): array + { + $data = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $composer = ComposerHelper::getComposerConfig($autoloadPath); + + if ($composer === null) { + continue; + } + + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; + if (!is_file($filePath)) { + continue; + } + + $installed = require $filePath; + $rootName = $installed['root']['name']; + unset($installed['root']); + unset($installed['versions'][$rootName]); + + $data[$filePath] = $installed; + } + + return $data; + } + /** * @return array */ private function getStubFiles(): array { $stubFiles = []; - foreach ($this->stubFiles as $stubFile) { + foreach ($this->stubFilesProvider->getProjectStubFiles() as $stubFile) { $stubFiles[$stubFile] = $this->getFileHash($stubFile); } + ksort($stubFiles); + return $stubFiles; } - public function clear(): string + /** + * @return array + * @throws ShouldNotHappenException + */ + private function getMetaFromPhpStanExtensions(): array { - $dir = dirname($this->cacheFilePath); - if (!is_file($this->cacheFilePath)) { - return $dir; + $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(); } - @unlink($this->cacheFilePath); + ksort($meta); - return $dir; + return $meta; } } diff --git a/src/Analyser/ResultCache/ResultCacheManagerFactory.php b/src/Analyser/ResultCache/ResultCacheManagerFactory.php new file mode 100644 index 0000000000..269f745015 --- /dev/null +++ b/src/Analyser/ResultCache/ResultCacheManagerFactory.php @@ -0,0 +1,10 @@ +analyserResult; + } + + public function isSaved(): bool + { + return $this->saved; + } + +} diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php new file mode 100644 index 0000000000..ba7c6c618a --- /dev/null +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -0,0 +1,82 @@ + + */ + 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 4b1f20235b..1134614b2f 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -2,92 +2,145 @@ namespace PHPStan\Analyser; +use PhpParser\Node; 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\Php\PhpFunctionFromParserNodeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; -interface Scope extends ClassMemberAccessAnswerer +/** @api */ +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 \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\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; public function getVariableType(string $variableName): Type; + public function canAnyVariableExist(): bool; + + /** + * @return array + */ + public function getDefinedVariables(): array; + + /** + * @return array + */ + public function getMaybeDefinedVariables(): array; + public function hasConstant(Name $name): bool; + public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection; + + 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; - public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type; + 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 - * @param Expr $expr - * @return Type - */ public function getNativeType(Expr $expr): Type; - public function doNotTreatPhpDocTypesAsCertain(): self; + public function getKeepVoidType(Expr $node): Type; public function resolveName(Name $name): string; + public function resolveTypeByName(Name $name): TypeWithClassName; + /** * @param mixed $value */ public function getTypeFromValue($value): Type; - public function isSpecified(Expr $node): bool; + public function hasExpressionType(Expr $node): TrinaryLogic; public function isInClassExists(string $className): bool; + public function isInFunctionExists(string $functionName): bool; + public function isInClosureBind(): bool; + /** @return list */ + public function getFunctionCallStack(): array; + + /** @return list */ + public function getFunctionCallStackWithParameters(): array; + public function isParameterValueNullable(Param $parameter): bool; /** - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $type - * @param bool $isNullable - * @param bool $isVariadic - * @return Type + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type; public function isInExpressionAssign(Expr $expr): bool; - public function filterByTruthyValue(Expr $expr, bool $defaultHandleFunctions = false): self; + public function isUndefinedExpressionAllowed(Expr $expr): bool; + + public function filterByTruthyValue(Expr $expr): self; - public function filterByFalseyValue(Expr $expr, bool $defaultHandleFunctions = false): 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 c2ff4dab33..dfa0c1f17b 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -3,27 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; -class ScopeContext +final class ScopeContext { - private string $file; - - private ?ClassReflection $classReflection; - - private ?ClassReflection $traitReflection; - private function __construct( - string $file, - ?ClassReflection $classReflection, - ?ClassReflection $traitReflection + private string $file, + private ?ClassReflection $classReflection, + private ?ClassReflection $traitReflection, ) { - $this->file = $file; - $this->classReflection = $classReflection; - $this->traitReflection = $traitReflection; } + /** @api */ public static function create(string $file): self { return new self($file, null, null); @@ -37,10 +30,10 @@ public function beginFile(): self public function enterClass(ClassReflection $classReflection): self { if ($this->classReflection !== null && !$classReflection->isAnonymous()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($classReflection->isTrait()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($this->file, $classReflection, null); } @@ -48,10 +41,10 @@ public function enterClass(ClassReflection $classReflection): self public function enterTrait(ClassReflection $traitReflection): self { if ($this->classReflection === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$traitReflection->isTrait()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($this->file, $this->classReflection, $traitReflection); diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index 374f47874e..ade6e1d894 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,45 +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 { - /** - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement - * @param array $currentlyAssignedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack - * - * @return MutatingScope - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [] - ): 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 501dc82afc..fd9ddda81d 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -2,37 +2,88 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class SpecifiedTypes +final class SpecifiedTypes { - /** @var mixed[] */ - private array $sureTypes; + private bool $overwrite = false; - /** @var mixed[] */ - private array $sureNotTypes; + /** @var array */ + private array $newConditionalExpressionHolders = []; - private bool $overwrite; + private ?Expr $rootExpr = null; /** - * @param mixed[] $sureTypes - * @param mixed[] $sureNotTypes - * @param bool $overwrite + * @api + * @param array $sureTypes + * @param array $sureNotTypes */ public function __construct( - array $sureTypes = [], - array $sureNotTypes = [], - bool $overwrite = false + private array $sureTypes = [], + private array $sureNotTypes = [], ) { - $this->sureTypes = $sureTypes; - $this->sureNotTypes = $sureNotTypes; - $this->overwrite = $overwrite; } /** - * @return mixed[] + * 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 */ public function getSureTypes(): array { @@ -40,7 +91,8 @@ public function getSureTypes(): array } /** - * @return mixed[] + * @api + * @return array */ public function getSureNotTypes(): array { @@ -52,10 +104,25 @@ public function shouldOverwrite(): bool return $this->overwrite; } + /** + * @return array + */ + 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])) { @@ -79,13 +146,20 @@ 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 */ 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])) { @@ -109,7 +183,46 @@ 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 + { + $sureTypes = $this->sureTypes; + + foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) { + if (!isset($sureTypes[$exprString])) { + $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($scope->getType($exprNode), $sureNotType)]; + continue; + } + + $sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType); + } + + $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 f5f6874438..5c4916373e 100644 --- a/src/Analyser/StatementExitPoint.php +++ b/src/Analyser/StatementExitPoint.php @@ -4,17 +4,14 @@ use PhpParser\Node\Stmt; -class StatementExitPoint +/** + * @api + */ +final class StatementExitPoint { - private Stmt $statement; - - private MutatingScope $scope; - - public function __construct(Stmt $statement, MutatingScope $scope) + public function __construct(private Stmt $statement, private MutatingScope $scope) { - $this->statement = $statement; - $this->scope = $scope; } public function getStatement(): Stmt diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index e589fb9434..dad528dc18 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -2,37 +2,31 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; -class StatementResult +/** + * @api + */ +final class StatementResult { - private MutatingScope $scope; - - private bool $hasYield; - - private bool $isAlwaysTerminating; - - /** @var StatementExitPoint[] */ - private array $exitPoints; - /** - * @param MutatingScope $scope - * @param bool $hasYield - * @param bool $isAlwaysTerminating * @param StatementExitPoint[] $exitPoints + * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param EndStatementResult[] $endStatements */ public function __construct( - MutatingScope $scope, - bool $hasYield, - bool $isAlwaysTerminating, - array $exitPoints + private MutatingScope $scope, + private bool $hasYield, + private bool $isAlwaysTerminating, + private array $exitPoints, + private array $throwPoints, + private array $impurePoints, + private array $endStatements = [], ) { - $this->scope = $scope; - $this->hasYield = $hasYield; - $this->isAlwaysTerminating = $isAlwaysTerminating; - $this->exitPoints = $exitPoints; } public function getScope(): MutatingScope @@ -58,12 +52,20 @@ public function filterOutLoopExitPoints(): self foreach ($this->exitPoints as $exitPoint) { $statement = $exitPoint->getStatement(); - if ( - $statement instanceof Stmt\Break_ - || $statement instanceof Stmt\Continue_ - ) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints); + if (!$statement instanceof Stmt\Break_ && !$statement instanceof Stmt\Continue_) { + continue; + } + + $num = $statement->num; + 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, $this->impurePoints); } return $this; @@ -78,14 +80,31 @@ public function getExitPoints(): array } /** - * @param string $stmtClass - * @return StatementExitPoint[] + * @param class-string|class-string $stmtClass + * @return list */ public function getExitPointsByType(string $stmtClass): array { $exitPoints = []; foreach ($this->exitPoints as $exitPoint) { - if (!$exitPoint->getStatement() instanceof $stmtClass) { + $statement = $exitPoint->getStatement(); + if (!$statement instanceof $stmtClass) { + continue; + } + + $value = $statement->num; + if ($value === null) { + $exitPoints[] = $exitPoint; + continue; + } + + if (!$value instanceof Int_) { + $exitPoints[] = $exitPoint; + continue; + } + + $value = $value->value; + if ($value !== 1) { continue; } @@ -95,4 +114,80 @@ public function getExitPointsByType(string $stmtClass): array return $exitPoints; } + /** + * @return list + */ + public function getExitPointsForOuterLoop(): array + { + $exitPoints = []; + foreach ($this->exitPoints as $exitPoint) { + $statement = $exitPoint->getStatement(); + if (!$statement instanceof Stmt\Continue_ && !$statement instanceof Stmt\Break_) { + $exitPoints[] = $exitPoint; + continue; + } + if ($statement->num === null) { + continue; + } + if (!$statement->num instanceof Int_) { + continue; + } + $value = $statement->num->value; + if ($value === 1) { + continue; + } + + $newNode = null; + if ($value > 2) { + $newNode = new Int_($value - 1); + } + if ($statement instanceof Stmt\Continue_) { + $newStatement = new Stmt\Continue_($newNode); + } else { + $newStatement = new Stmt\Break_($newNode); + } + + $exitPoints[] = new StatementExitPoint($newStatement, $exitPoint->getScope()); + } + + return $exitPoints; + } + + /** + * @return ThrowPoint[] + */ + 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 new file mode 100644 index 0000000000..873c11e425 --- /dev/null +++ b/src/Analyser/ThrowPoint.php @@ -0,0 +1,79 @@ +scope; + } + + public function getType(): Type + { + return $this->type; + } + + /** + * @return Node\Expr|Node\Stmt + */ + public function getNode() + { + return $this->node; + } + + public function isExplicit(): bool + { + return $this->explicit; + } + + public function canContainAnyThrowable(): bool + { + return $this->canContainAnyThrowable; + } + + public function subtractCatchType(Type $catchType): self + { + return new self($this->scope, TypeCombinator::remove($this->type, $catchType), $this->node, $this->explicit, $this->canContainAnyThrowable); + } + +} diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3b6d580a5d..405b4a9adc 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; @@ -9,84 +10,106 @@ use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; use PhpParser\Node\Expr\BinaryOp\LogicalOr; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; +use 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\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; +use PHPStan\Type\MethodTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\ResourceType; +use PHPStan\Type\StaticMethodTypeSpecifyingExtension; use PHPStan\Type\StaticType; +use PHPStan\Type\StaticTypeFactory; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; - -class TypeSpecifier +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; + +final class TypeSpecifier { - private \PhpParser\PrettyPrinter\Standard $printer; - - private ReflectionProvider $reflectionProvider; - - /** @var \PHPStan\Type\FunctionTypeSpecifyingExtension[] */ - private array $functionTypeSpecifyingExtensions; - - /** @var \PHPStan\Type\MethodTypeSpecifyingExtension[] */ - private array $methodTypeSpecifyingExtensions; - - /** @var \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] */ - private array $staticMethodTypeSpecifyingExtensions; - - /** @var \PHPStan\Type\MethodTypeSpecifyingExtension[][]|null */ + /** @var MethodTypeSpecifyingExtension[][]|null */ private ?array $methodTypeSpecifyingExtensionsByClass = null; - /** @var \PHPStan\Type\StaticMethodTypeSpecifyingExtension[][]|null */ + /** @var StaticMethodTypeSpecifyingExtension[][]|null */ private ?array $staticMethodTypeSpecifyingExtensionsByClass = null; /** - * @param \PhpParser\PrettyPrinter\Standard $printer - * @param ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions + * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions + * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions + * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions */ public function __construct( - \PhpParser\PrettyPrinter\Standard $printer, - ReflectionProvider $reflectionProvider, - array $functionTypeSpecifyingExtensions, - array $methodTypeSpecifyingExtensions, - array $staticMethodTypeSpecifyingExtensions + private ExprPrinter $exprPrinter, + private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, + private array $functionTypeSpecifyingExtensions, + private array $methodTypeSpecifyingExtensions, + private array $staticMethodTypeSpecifyingExtensions, + private bool $rememberPossiblyImpureFunctionValues, ) { - $this->printer = $printer; - $this->reflectionProvider = $reflectionProvider; - foreach (array_merge($functionTypeSpecifyingExtensions, $methodTypeSpecifyingExtensions, $staticMethodTypeSpecifyingExtensions) as $extension) { if (!($extension instanceof TypeSpecifierAwareExtension)) { continue; @@ -94,35 +117,32 @@ public function __construct( $extension->setTypeSpecifier($this); } - - $this->functionTypeSpecifyingExtensions = $functionTypeSpecifyingExtensions; - $this->methodTypeSpecifyingExtensions = $methodTypeSpecifyingExtensions; - $this->staticMethodTypeSpecifyingExtensions = $staticMethodTypeSpecifyingExtensions; } + /** @api */ public function specifyTypesInCondition( Scope $scope, Expr $expr, TypeSpecifierContext $context, - bool $defaultHandleFunctions = false ): SpecifiedTypes { + if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + if ($expr instanceof Instanceof_) { $exprNode = $expr->expr; - if ($exprNode instanceof Expr\Assign) { - $exprNode = $exprNode->var; - } if ($expr->class instanceof Name) { $className = (string) $expr->class; $lowercasedClassName = strtolower($className); if ($lowercasedClassName === 'self' && $scope->isInClass()) { $type = new ObjectType($scope->getClassReflection()->getName()); } elseif ($lowercasedClassName === 'static' && $scope->isInClass()) { - $type = new StaticType($scope->getClassReflection()->getName()); + $type = new StaticType($scope->getClassReflection()); } elseif ($lowercasedClassName === 'parent') { if ( $scope->isInClass() - && $scope->getClassReflection()->getParentClass() !== false + && $scope->getClassReflection()->getParentClass() !== null ) { $type = new ObjectType($scope->getClassReflection()->getParentClass()->getName()); } else { @@ -131,18 +151,21 @@ public function specifyTypesInCondition( } else { $type = new ObjectType($className); } - return $this->create($exprNode, $type, $context); + 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) { @@ -155,265 +178,301 @@ public function specifyTypesInCondition( if ($context->true()) { $type = TypeCombinator::intersect( $type, - new ObjectWithoutClassType() + new ObjectWithoutClassType(), ); + 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, $scope)->setRootExpr($expr); + } } - return $this->create($exprNode, $type, $context); } if ($context->true()) { - return $this->create($exprNode, new ObjectWithoutClassType(), $context); + 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]; - if ($exprNode instanceof Expr\Assign) { - $exprNode = $exprNode->var; - } - /** @var \PHPStan\Type\ConstantScalarType $constantType */ - $constantType = $expressions[1]; - if ($constantType->getValue() === false) { - $types = $this->create($exprNode, $constantType, $context); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate() - )); - } + return $this->resolveIdentical($expr, $scope, $context); - if ($constantType->getValue() === true) { - $types = $this->create($exprNode, $constantType, $context); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate() - )); - } + } 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) { + 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); - if ($constantType->getValue() === null) { - return $this->create($exprNode, $constantType, $context); - } + } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->args) === 1 - && $exprNode->name instanceof Name - && strtolower((string) $exprNode->name) === 'count' - && $constantType instanceof ConstantIntegerType - ) { - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->args[0]->value); - if ((new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($argType)->yes()) { - return $this->create($exprNode->args[0]->value, new NonEmptyArrayType(), $newContext); - } - } - } + if ( + $expr->left instanceof FuncCall + && count($expr->left->getArgs()) >= 1 + && $expr->left->name instanceof Name + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + && ( + !$expr->right instanceof FuncCall + || !$expr->right->name instanceof Name + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + ) + ) { + $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller + ? new Node\Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left) + : new Node\Expr\BinaryOp\Smaller($expr->right, $expr->left); + + return $this->specifyTypesInCondition( + $scope, + new Node\Expr\BooleanNot($inverseOperator), + $context, + )->setRootExpr($expr); } - if ($context->true()) { - $type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left)); - $leftTypes = $this->create($expr->left, $type, $context); - $rightTypes = $this->create($expr->right, $type, $context); - return $leftTypes->unionWith($rightTypes); + $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + $leftType = $scope->getType($expr->left); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); - } elseif ($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); - $rightTypes = $this->create($expr->right, $never, $contextForTypes); - return $leftTypes->unionWith($rightTypes); + if ( + !$context->null() + && $expr->right instanceof FuncCall + && count($expr->right->getArgs()) >= 1 + && $expr->right->name instanceof Name + && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) + && $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; } - if ( - ( - $expr->left instanceof Node\Scalar - || $expr->left instanceof Expr\Array_ - ) - && !$expr->right instanceof Node\Scalar - ) { - return $this->create( - $expr->right, - $scope->getType($expr->left), - $context - ); + $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); } + if ( - ( - $expr->right instanceof Node\Scalar - || $expr->right instanceof Expr\Array_ - ) - && !$expr->left instanceof Node\Scalar + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - return $this->create( - $expr->left, - $scope->getType($expr->right), - $context - ); - } - } + 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; + } + } - } 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 - ); - } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var \PHPStan\Type\ConstantScalarType $constantType */ - $constantType = $expressions[1]; - if ($constantType->getValue() === false || $constantType->getValue() === null) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate() - ); - } + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); - if ($constantType->getValue() === true) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate() - ); - } - } + return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); + } + } - $leftType = $scope->getType($expr->left); - $leftBooleanType = $leftType->toBoolean(); - $rightType = $scope->getType($expr->right); - 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 - ); - } + if ($argType->isArray()->yes()) { + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); + } - $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 - ); + $result = $result->unionWith( + $this->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), + ); + } + } } if ( - $expr->left instanceof FuncCall - && $expr->left->name instanceof Name - && strtolower($expr->left->name->toString()) === 'get_class' - && isset($expr->left->args[0]) - && $rightType instanceof ConstantStringType + !$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 Instanceof_( - $expr->left->args[0]->value, - new Name($rightType->getValue()) - ), - $context - ); + new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))), + $context, + )->setRootExpr($expr); } if ( - $expr->right instanceof FuncCall + !$context->null() + && $expr->right instanceof FuncCall + && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name - && strtolower($expr->right->name->toString()) === 'get_class' - && isset($expr->right->args[0]) - && $leftType instanceof ConstantStringType + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && $leftType->isInteger()->yes() ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $expr->right->args[0]->value, - new Name($leftType->getValue()) - ), - $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 - ); + if ( + $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->isString()->yes()) { + $accessory = new AccessoryNonEmptyStringType(); - } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { - $offset = $expr instanceof Node\Expr\BinaryOp\Smaller ? 1 : 0; - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } - $result = new SpecifiedTypes(); + $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); + } + } + } if ($leftType instanceof ConstantIntegerType) { - if ($expr->right instanceof Expr\PostDec) { + 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() + $offset - 1, null), - $context + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), + $context, )); - } elseif ($expr->right instanceof Expr\PreDec) { + } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( + $expr, $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue() + $offset, null), - $context + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), + $context, )); } - - $result = $result->unionWith($this->createRangeTypes( - $expr->right, - IntegerRangeType::fromInterval($leftType->getValue() + $offset, null), - $context - )); } + $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 + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), + $context, )); - } elseif ($expr->left instanceof Expr\PreInc) { + } 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 + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), + $context, )); } + } - $result = $result->unionWith($this->createRangeTypes( - $expr->left, - IntegerRangeType::fromInterval(null, $rightType->getValue() - $offset), - $context - )); + if ($context->true()) { + if (!$expr->left instanceof Node\Scalar) { + $result = $result->unionWith( + $this->create( + $expr->left, + $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Node\Scalar) { + $result = $result->unionWith( + $this->create( + $expr->right, + $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + } elseif ($context->false()) { + if (!$expr->left instanceof Node\Scalar) { + $result = $result->unionWith( + $this->create( + $expr->left, + $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Node\Scalar) { + $result = $result->unionWith( + $this->create( + $expr->right, + $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } } return $result; } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context, $defaultHandleFunctions); + 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, $defaultHandleFunctions); + 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)) { @@ -425,21 +484,46 @@ public function specifyTypesInCondition( return $extension->specifyTypes($functionReflection, $expr, $scope, $context); } - } - if ($defaultHandleFunctions) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr); + // 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; @@ -448,21 +532,46 @@ public function specifyTypesInCondition( return $extension->specifyTypes($methodReflection, $expr, $scope, $context); } } - } - if ($defaultHandleFunctions) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr); + // 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); } elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) { if ($expr->class instanceof Name) { - $calleeType = new ObjectType($scope->resolveName($expr->class)); + $calleeType = $scope->resolveTypeByName($expr->class); } else { $calleeType = $scope->getType($expr->class); } - if ($calleeType->hasMethod($expr->name->name)->yes()) { - $staticMethodReflection = $calleeType->getMethod($expr->name->name, $scope); - $referencedClasses = TypeUtils::getDirectClassNames($calleeType); + $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); + if ($staticMethodReflection !== null) { + $referencedClasses = $calleeType->getObjectClassNames(); if ( count($referencedClasses) === 1 && $this->reflectionProvider->hasClass($referencedClasses[0]) @@ -476,227 +585,1337 @@ public function specifyTypesInCondition( return $extension->specifyTypes($staticMethodReflection, $expr, $scope, $context); } } - } - if ($defaultHandleFunctions) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr); + // 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); } elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) { - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); - $rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context); - return $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes); - } elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) { - $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); - $rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context); - return $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes); - } elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate()); - } elseif ($expr instanceof Node\Expr\Assign) { if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ($context->null()) { - return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context); + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); + $rightScope = $scope->filterByTruthyValue($expr->left); + $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( + $types->getSureTypes(), + $types->getSureNotTypes(), + ))->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 $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context); - } elseif ( - ( - $expr instanceof Expr\Isset_ - && count($expr->vars) > 0 - && $context->truthy() - ) - || ($expr instanceof Expr\Empty_ && $context->falsey()) - ) { - $vars = []; - if ($expr instanceof Expr\Isset_) { - $varsToIterate = $expr->vars; - } else { - $varsToIterate = [$expr->expr]; + return $types; + } elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); } - foreach ($varsToIterate as $var) { - $vars[] = $var; - - 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; - } - $vars[] = $var; - } + $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); + $rightScope = $scope->filterByFalseyValue($expr->left); + $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( + $types->getSureTypes(), + $types->getSureNotTypes(), + ))->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); } - if (count($vars) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + return $types; + } elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) { + return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr); + } elseif ($expr instanceof Node\Expr\Assign) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); } - $types = null; - foreach ($vars as $var) { - if ($expr instanceof Expr\Isset_) { + if ($context->null()) { + $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 ( - $var instanceof ArrayDimFetch - && $var->dim !== null - && !$scope->getType($var->var) instanceof MixedType + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() ) { - $type = $this->create( - $var->var, - new HasOffsetType($scope->getType($var->dim)), - $context - )->unionWith( - $this->create($var, new NullType(), TypeSpecifierContext::createFalse()) + $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), ); - } else { - $type = $this->create($var, new NullType(), TypeSpecifierContext::createFalse()); } - } else { - $type = $this->create( - $var, - new UnionType([ - new NullType(), - new ConstantBooleanType(false), - ]), - TypeSpecifierContext::createFalse() - ); } + // infer $list[$count] after $count = count($list) - 1 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())); - } elseif ( - $var instanceof StaticPropertyFetch - && $var->class instanceof Expr - && $var->name instanceof Node\VarLikeIdentifier + $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 ) { - $type = $type->unionWith($this->create($var->class, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy())); - } + $arrayArg = $expr->expr->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - if ($types === null) { - $types = $type; - } else { - $types = $types->unionWith($type); + return $specifiedTypes->unionWith( + $this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope), + ); + } } - } - if ( - $expr instanceof Expr\Empty_ - && (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes()) { - $types = $types->unionWith( - $this->create($expr->expr, new NonEmptyArrayType(), $context->negate()) - ); + return $specifiedTypes; } - return $types; + $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\Empty_ && $context->truthy() - && (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes() + $expr instanceof Expr\Isset_ + && count($expr->vars) > 0 + && !$context->null() ) { - return $this->create($expr->expr, new NonEmptyArrayType(), $context->negate()); - } elseif ($expr instanceof Expr\ErrorSuppress) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context, $defaultHandleFunctions); - } elseif (!$context->null()) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr); - } + // 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()); + } - return new SpecifiedTypes(); - } + $first = array_shift($issets); + $andChain = null; + foreach ($issets as $isset) { + if ($andChain === null) { + $andChain = new BooleanAnd($first, $isset); + continue; + } - private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr): SpecifiedTypes + $andChain = new BooleanAnd($andChain, $isset); + } + + if ($andChain === null) { + throw new ShouldNotHappenException(); + } + + return $this->specifyTypesInCondition($scope, $andChain, $context)->setRootExpr($expr); + } + + $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 + ) { + $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 + ) { + $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 + ) { + $types = $types->unionWith( + $this->create($var->class, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr), + ); + } + + $types = $types->unionWith( + $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr), + ); + } + + return $types; + } elseif ( + $expr instanceof Expr\BinaryOp\Coalesce + && !$context->null() + ) { + 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)->setRootExpr($expr); + } elseif ($expr instanceof Expr\ErrorSuppress) { + return $this->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr); + } elseif ( + $expr instanceof Expr\Ternary + && !$context->null() + && $scope->getType($expr->else)->isFalse()->yes() + ) { + $conditionExpr = $expr->cond; + if ($expr->if !== null) { + $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); + } + + return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); + + } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) { + $types = $this->specifyTypesInCondition( + $scope, + new BooleanAnd( + new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), + 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)); + } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) { + $types = $this->specifyTypesInCondition( + $scope, + new BooleanAnd( + new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), + new MethodCall($expr->var, $expr->name, $expr->args), + ), + $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([], []))->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([], []))->setRootExpr($expr); + } if (!$context->truthy()) { - $type = new UnionType([ - new ObjectWithoutClassType(), - new NonEmptyArrayType(), - new ConstantBooleanType(true), - ]); - return $this->create($expr, $type, TypeSpecifierContext::createFalse()); + $type = StaticTypeFactory::truthy(); + return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr); } elseif (!$context->falsey()) { - $type = new UnionType([ - new NullType(), - new ConstantBooleanType(false), - new ConstantIntegerType(0), - new ConstantFloatType(0.0), - new ConstantStringType(''), - new ConstantArrayType([], []), - ]); - return $this->create($expr, $type, TypeSpecifierContext::createFalse()); - } - - return new SpecifiedTypes(); + $type = StaticTypeFactory::falsey(); + 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 []; + } + + /** + * @return array + */ + private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + { + $conditionExpressionTypes = []; + foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::intersect($scope->getType($expr), $type), + ); + } + + if (count($conditionExpressionTypes) > 0) { + $holders = []; + foreach ($rightTypes->getSureNotTypes() 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::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), + ); + $holders[$exprString][$holder->getKey()] = $holder; + } + + return $holders; + } + + return []; } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr\BinaryOp $binaryOperation - * @return (Expr|\PHPStan\Type\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 = $this->extractExpression($binaryOperation->right); + $leftExpr = $this->extractExpression($binaryOperation->left); + if ( - $leftType instanceof \PHPStan\Type\ConstantScalarType - && !$binaryOperation->right instanceof ConstFetch - && !$binaryOperation->right instanceof Expr\ClassConstFetch + $leftType instanceof ConstantScalarType + && !$rightExpr instanceof ConstFetch + && !$rightExpr instanceof ClassConstFetch ) { - return [$binaryOperation->right, $leftType]; + return [$binaryOperation->right, $leftType, $rightType]; } elseif ( - $rightType instanceof \PHPStan\Type\ConstantScalarType - && !$binaryOperation->left instanceof ConstFetch - && !$binaryOperation->left instanceof Expr\ClassConstFetch + $rightType instanceof ConstantScalarType + && !$leftExpr instanceof ConstFetch + && !$leftExpr instanceof ClassConstFetch + ) { + return [$binaryOperation->left, $rightType, $leftType]; + } + + return null; + } + + /** + * @return array{Expr, Type, Type}|null + */ + private function findEnumTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array + { + $leftType = $scope->getType($binaryOperation->left); + $rightType = $scope->getType($binaryOperation->right); + + $rightExpr = $this->extractExpression($binaryOperation->right); + $leftExpr = $this->extractExpression($binaryOperation->left); + + if ( + $leftType->getEnumCases() === [$leftType] + && !$rightExpr instanceof ConstFetch + && !$rightExpr instanceof ClassConstFetch + ) { + return [$binaryOperation->right, $leftType, $rightType]; + } elseif ( + $rightType->getEnumCases() === [$rightType] + && !$leftExpr instanceof ConstFetch + && !$leftExpr instanceof ClassConstFetch + ) { + return [$binaryOperation->left, $rightType, $leftType]; + } + + return null; + } + + private function extractExpression(Expr $expr): Expr + { + return $expr instanceof AlwaysRememberedExpr ? $expr->getExpr() : $expr; + } + + /** @api */ + public function create( + Expr $expr, + Type $type, + TypeSpecifierContext $context, + Scope $scope, + ): SpecifiedTypes + { + if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + $specifiedExprs = []; + if ($expr instanceof AlwaysRememberedExpr) { + $specifiedExprs[] = $expr; + $expr = $expr->expr; + } + + if ($expr instanceof Expr\Assign) { + $specifiedExprs[] = $expr->var; + $specifiedExprs[] = $expr->expr; + + while ($expr->expr instanceof Expr\Assign) { + $specifiedExprs[] = $expr->expr->var; + $expr = $expr->expr; + } + } elseif ($expr instanceof Expr\AssignOp\Coalesce) { + $specifiedExprs[] = $expr->var; + } else { + $specifiedExprs[] = $expr; + } + + $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($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 + ) { + $has = $this->reflectionProvider->hasFunction($expr->name, $scope); + if (!$has) { + // backwards compatibility with previous behaviour + return new SpecifiedTypes([], []); + } + + $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); + $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 + ) { + $methodName = $expr->name->toString(); + $calledOnType = $scope->getType($expr->var); + $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([], []); + } + } + + if ( + $expr instanceof StaticCall + && $expr->name instanceof Node\Identifier ) { - return [$binaryOperation->left, $rightType]; - } + $methodName = $expr->name->toString(); + if ($expr->class instanceof Name) { + $calledOnType = $scope->resolveTypeByName($expr->class); + } else { + $calledOnType = $scope->getType($expr->class); + } - return null; - } + $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); + } - public function create( - Expr $expr, - Type $type, - TypeSpecifierContext $context, - bool $overwrite = false - ): SpecifiedTypes - { - if ($expr instanceof New_ || $expr instanceof Instanceof_) { - return new SpecifiedTypes(); + 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); + if (isset($containsNull) && !$containsNull) { + return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types); + } + + return $types; + } + + private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes + { + if ($expr instanceof Expr\NullsafePropertyFetch) { + if ($type !== null) { + $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(), $scope); + } + + return $propertyFetchTypes->unionWith( + $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, $scope); + } else { + $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(), $scope), + ); + } + + if ($expr instanceof Expr\PropertyFetch) { + return $this->createNullsafeTypes($expr->var, $scope, $context, null); + } + + if ($expr instanceof Expr\MethodCall) { + return $this->createNullsafeTypes($expr->var, $scope, $context, null); } - return new SpecifiedTypes($sureTypes, $sureNotTypes, $overwrite); + if ($expr instanceof Expr\ArrayDimFetch) { + return $this->createNullsafeTypes($expr->var, $scope, $context, null); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->createNullsafeTypes($expr->class, $scope, $context, null); + } + + if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { + return $this->createNullsafeTypes($expr->class, $scope, $context, null); + } + + 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()) { @@ -705,11 +1924,11 @@ private function createRangeTypes(Expr $expr, Type $type, TypeSpecifierContext $ } } - return new SpecifiedTypes([], $sureNotTypes); + return (new SpecifiedTypes([], $sureNotTypes))->setRootExpr($rootExpr); } /** - * @return \PHPStan\Type\FunctionTypeSpecifyingExtension[] + * @return FunctionTypeSpecifyingExtension[] */ private function getFunctionTypeSpecifyingExtensions(): array { @@ -717,8 +1936,7 @@ private function getFunctionTypeSpecifyingExtensions(): array } /** - * @param string $className - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] + * @return MethodTypeSpecifyingExtension[] */ private function getMethodTypeSpecifyingExtensionsForClass(string $className): array { @@ -734,8 +1952,7 @@ private function getMethodTypeSpecifyingExtensionsForClass(string $className): a } /** - * @param string $className - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] + * @return StaticMethodTypeSpecifyingExtension[] */ private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array { @@ -751,8 +1968,7 @@ private function getStaticMethodTypeSpecifyingExtensionsForClass(string $classNa } /** - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[][]|\PHPStan\Type\StaticMethodTypeSpecifyingExtension[][] $extensions - * @param string $className + * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions * @return mixed[] */ private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array @@ -770,4 +1986,591 @@ 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); + } + + if (!$context->null() && TypeCombinator::containsNull($otherType)) { + if ($constantType->toBoolean()->isTrue()->yes()) { + $otherType = TypeCombinator::remove($otherType, new NullType()); + } + + if (!$otherType->isSuperTypeOf($constantType)->no()) { + return $this->create($exprNode, TypeCombinator::intersect($constantType, $otherType), $context, $scope)->setRootExpr($expr); + } + } + } + + $expressions = $this->findEnumTypeExpressionsFromBinaryOperation($scope, $expr); + if ($expressions !== null) { + $exprNode = $expressions[0]; + $enumCaseObjectType = $expressions[1]; + $otherType = $expressions[2]; + + if (!$context->null()) { + return $this->create($exprNode, TypeCombinator::intersect($enumCaseObjectType, $otherType), $context, $scope)->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/TypeSpecifierAwareExtension.php b/src/Analyser/TypeSpecifierAwareExtension.php index 88fc2e4243..470ca683be 100644 --- a/src/Analyser/TypeSpecifierAwareExtension.php +++ b/src/Analyser/TypeSpecifierAwareExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +/** @api */ interface TypeSpecifierAwareExtension { diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index c07160a50e..fe09aa861c 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -2,7 +2,12 @@ namespace PHPStan\Analyser; -class TypeSpecifierContext +use PHPStan\ShouldNotHappenException; + +/** + * @api + */ +final class TypeSpecifierContext { public const CONTEXT_TRUE = 0b0001; @@ -11,20 +16,18 @@ class TypeSpecifierContext public const CONTEXT_FALSE = 0b0100; public const CONTEXT_FALSEY_BUT_NOT_FALSE = 0b1000; public const CONTEXT_FALSEY = self::CONTEXT_FALSE | self::CONTEXT_FALSEY_BUT_NOT_FALSE; - - private ?int $value; + public const CONTEXT_BITMASK = 0b1111; /** @var self[] */ private static array $registry; - private function __construct(?int $value) + private function __construct(private ?int $value) { - $this->value = $value; } private static function create(?int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -56,9 +59,9 @@ public static function createNull(): self public function negate(): self { if ($this->value === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return self::create(~$this->value); + return self::create(~$this->value & self::CONTEXT_BITMASK); } public function true(): bool diff --git a/src/Analyser/TypeSpecifierFactory.php b/src/Analyser/TypeSpecifierFactory.php index 1873493047..83315b6e0c 100644 --- a/src/Analyser/TypeSpecifierFactory.php +++ b/src/Analyser/TypeSpecifierFactory.php @@ -2,33 +2,34 @@ 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'; public const METHOD_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.typeSpecifier.methodTypeSpecifyingExtension'; public const STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension'; - private \PHPStan\DependencyInjection\Container $container; - - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function create(): TypeSpecifier { $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->getServicesByTag(self::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG), + $this->container->getParameter('rememberPossiblyImpureFunctionValues'), ); foreach (array_merge( @@ -36,7 +37,7 @@ public function create(): TypeSpecifier $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), $this->container->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $this->container->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG), - $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG) + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), ) as $extension) { if (!($extension instanceof TypeSpecifierAwareExtension)) { continue; diff --git a/src/Analyser/UndefinedVariableException.php b/src/Analyser/UndefinedVariableException.php index 55be8974c0..4755296c6b 100644 --- a/src/Analyser/UndefinedVariableException.php +++ b/src/Analyser/UndefinedVariableException.php @@ -2,18 +2,15 @@ namespace PHPStan\Analyser; -class UndefinedVariableException extends \PHPStan\AnalysedCodeException -{ - - private \PHPStan\Analyser\Scope $scope; +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $variableName; +final class UndefinedVariableException extends AnalysedCodeException +{ - public function __construct(Scope $scope, string $variableName) + public function __construct(private Scope $scope, private string $variableName) { parent::__construct(sprintf('Undefined variable: $%s', $variableName)); - $this->scope = $scope; - $this->variableName = $variableName; } public function getScope(): Scope diff --git a/src/Analyser/VariableTypeHolder.php b/src/Analyser/VariableTypeHolder.php deleted file mode 100644 index edd73bb833..0000000000 --- a/src/Analyser/VariableTypeHolder.php +++ /dev/null @@ -1,58 +0,0 @@ -no()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $this->type = $type; - $this->certainty = $certainty; - } - - public static function createYes(Type $type): self - { - return new self($type, TrinaryLogic::createYes()); - } - - public static function createMaybe(Type $type): self - { - return new self($type, TrinaryLogic::createMaybe()); - } - - 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 b7040689a2..0f9e82cb21 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -2,41 +2,51 @@ namespace PHPStan\Broker; +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 { - private FileHelper $fileHelper; - - private RelativePathHelper $relativePathHelper; - public function __construct( - FileHelper $fileHelper, - RelativePathHelper $relativePathHelper + private FileHelper $fileHelper, + private RelativePathHelper $relativePathHelper, ) { - $this->fileHelper = $fileHelper; - $this->relativePathHelper = $relativePathHelper; } + /** + * @return non-empty-string + */ public function getAnonymousClassName( - \PhpParser\Node\Stmt\Class_ $classNode, - string $filename + Node\Stmt\Class_ $classNode, + string $filename, ): string { if (isset($classNode->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $filename = $this->relativePathHelper->getRelativePath( - $this->fileHelper->normalizePath($filename, '/') + $this->fileHelper->normalizePath($filename, '/'), ); + /** @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 5920460e2d..0000000000 --- a/src/Broker/Broker.php +++ /dev/null @@ -1,171 +0,0 @@ -reflectionProvider = $reflectionProvider; - $this->dynamicReturnTypeExtensionRegistryProvider = $dynamicReturnTypeExtensionRegistryProvider; - $this->operatorTypeSpecifyingExtensionRegistryProvider = $operatorTypeSpecifyingExtensionRegistryProvider; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; - } - - public static function registerInstance(Broker $reflectionProvider): void - { - self::$instance = $reflectionProvider; - } - - public static function getInstance(): Broker - { - if (self::$instance === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - return self::$instance; - } - - public function hasClass(string $className): bool - { - return $this->reflectionProvider->hasClass($className); - } - - public function getClass(string $className): ClassReflection - { - return $this->reflectionProvider->getClass($className); - } - - public function getClassName(string $className): string - { - return $this->reflectionProvider->getClassName($className); - } - - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - return $this->reflectionProvider->getAnonymousClassReflection($classNode, $scope); - } - - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasFunction($nameNode, $scope); - } - - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $scope); - } - - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); - } - - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $scope); - } - - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $scope); - } - - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveConstantName($nameNode, $scope); - } - - /** - * @return string[] - */ - public function getUniversalObjectCratesClasses(): array - { - return $this->universalObjectCratesClasses; - } - - /** - * @param string $className - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] - */ - public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array - { - return $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicMethodReturnTypeExtensionsForClass($className); - } - - /** - * @param string $className - * @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] - */ - public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $className): array - { - return $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicStaticMethodReturnTypeExtensionsForClass($className); - } - - /** - * @return OperatorTypeSpecifyingExtension[] - */ - public function getOperatorTypeSpecifyingExtensions(string $operator, Type $leftType, Type $rightType): array - { - return $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()->getOperatorTypeSpecifyingExtensions($operator, $leftType, $rightType); - } - - /** - * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] - */ - public function getDynamicFunctionReturnTypeExtensions(): array - { - return $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicFunctionReturnTypeExtensions(); - } - - /** - * @internal - * @return DynamicReturnTypeExtensionRegistryProvider - */ - public function getDynamicReturnTypeExtensionRegistryProvider(): DynamicReturnTypeExtensionRegistryProvider - { - return $this->dynamicReturnTypeExtensionRegistryProvider; - } - - /** - * @internal - * @return \PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider - */ - public function getOperatorTypeSpecifyingExtensionRegistryProvider(): OperatorTypeSpecifyingExtensionRegistryProvider - { - return $this->operatorTypeSpecifyingExtensionRegistryProvider; - } - -} diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index 18eba1875c..bbd8d97a3d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -2,36 +2,16 @@ namespace PHPStan\Broker; -use PHPStan\DependencyInjection\Container; -use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; -use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; -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'; - - private \PHPStan\DependencyInjection\Container $container; - - public function __construct(Container $container) - { - $this->container = $container; - } - - public function create(): Broker - { - return new Broker( - $this->container->getByType(ReflectionProvider::class), - $this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), - $this->container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), - $this->container->getParameter('universalObjectCratesClasses') - ); - } + public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension'; } diff --git a/src/Broker/ClassAutoloadingException.php b/src/Broker/ClassAutoloadingException.php index 9898953138..5451987023 100644 --- a/src/Broker/ClassAutoloadingException.php +++ b/src/Broker/ClassAutoloadingException.php @@ -2,14 +2,19 @@ namespace PHPStan\Broker; -class ClassAutoloadingException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use Throwable; +use function get_class; +use function sprintf; + +final class ClassAutoloadingException extends AnalysedCodeException { private string $className; public function __construct( string $functionName, - ?\Throwable $previous = null + ?Throwable $previous = null, ) { if ($previous !== null) { @@ -17,12 +22,12 @@ public function __construct( '%s (%s) thrown while looking for class %s.', get_class($previous), $previous->getMessage(), - $functionName + $functionName, ), 0, $previous); } else { parent::__construct(sprintf( 'Class %s not found.', - $functionName + $functionName, ), 0); } @@ -34,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 1afb8d7458..1276d663ff 100644 --- a/src/Broker/ClassNotFoundException.php +++ b/src/Broker/ClassNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\Broker; -class ClassNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $className; +final class ClassNotFoundException extends AnalysedCodeException +{ - public function __construct(string $functionName) + public function __construct(private string $className) { - parent::__construct(sprintf('Class %s was not found while trying to analyse it - discovering symbols is probably not configured properly.', $functionName)); - $this->className = $functionName; + parent::__construct(sprintf('Class %s was not found while trying to analyse it - discovering symbols is probably not configured properly.', $className)); } public function getClassName(): string @@ -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 25a3f7b775..5d633de380 100644 --- a/src/Broker/ConstantNotFoundException.php +++ b/src/Broker/ConstantNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\Broker; -class ConstantNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $constantName; +final class ConstantNotFoundException extends AnalysedCodeException +{ - public function __construct(string $constantName) + public function __construct(private string $constantName) { parent::__construct(sprintf('Constant %s not found.', $constantName)); - $this->constantName = $constantName; } public function getConstantName(): string @@ -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 f3966b1ef8..a313b60e2a 100644 --- a/src/Broker/FunctionNotFoundException.php +++ b/src/Broker/FunctionNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\Broker; -class FunctionNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $functionName; +final class FunctionNotFoundException extends AnalysedCodeException +{ - public function __construct(string $functionName) + public function __construct(private string $functionName) { parent::__construct(sprintf('Function %s not found while trying to analyse it - discovering symbols is probably not configured properly.', $functionName)); - $this->functionName = $functionName; } public function getFunctionName(): string @@ -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 1447662c58..a4b66596fa 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -2,18 +2,14 @@ namespace PHPStan\Cache; -class Cache +final class Cache { - private \PHPStan\Cache\CacheStorage $storage; - - public function __construct(CacheStorage $storage) + public function __construct(private CacheStorage $storage) { - $this->storage = $storage; } /** - * @param string $key * @return mixed|null */ public function load(string $key, string $variableKey) @@ -22,10 +18,7 @@ public function load(string $key, string $variableKey) } /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void { diff --git a/src/Cache/CacheItem.php b/src/Cache/CacheItem.php index 17114f5fc5..101bbfe2fd 100644 --- a/src/Cache/CacheItem.php +++ b/src/Cache/CacheItem.php @@ -2,22 +2,14 @@ namespace PHPStan\Cache; -class CacheItem +final class CacheItem { - private string $variableKey; - - /** @var mixed */ - private $data; - /** - * @param string $variableKey * @param mixed $data */ - public function __construct(string $variableKey, $data) + public function __construct(private string $variableKey, private $data) { - $this->variableKey = $variableKey; - $this->data = $data; } public function isVariableKeyValid(string $variableKey): bool @@ -35,7 +27,6 @@ public function getData() /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { diff --git a/src/Cache/CacheStorage.php b/src/Cache/CacheStorage.php index a9227b0eca..c3a645eb2b 100644 --- a/src/Cache/CacheStorage.php +++ b/src/Cache/CacheStorage.php @@ -6,17 +6,12 @@ interface CacheStorage { /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey); /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void; diff --git a/src/Cache/FileCacheStorage.php b/src/Cache/FileCacheStorage.php index c5597e51ce..1b66f26e2a 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -2,56 +2,55 @@ namespace PHPStan\Cache; +use InvalidArgumentException; use Nette\Utils\Random; +use PHPStan\File\CouldNotReadFileException; +use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileReader; use PHPStan\File\FileWriter; - -class FileCacheStorage implements CacheStorage +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\ShouldNotHappenException; +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 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; + +final class FileCacheStorage implements CacheStorage { - private string $directory; - - public function __construct(string $directory) - { - $this->directory = $directory; - } + private const CACHED_CLEARED_VERSION = 'v2-new'; - public function makeRootDir(): void + public function __construct(private string $directory) { - $this->makeDir($this->directory); - } - - private function makeDir(string $directory): void - { - if (is_dir($directory)) { - return; - } - - $result = @mkdir($directory, 0777); - if ($result === false) { - clearstatcache(); - if (is_dir($directory)) { - return; - } - - $error = error_get_last(); - throw new \InvalidArgumentException(sprintf('Failed to create directory "%s" (%s).', $this->directory, $error !== null ? $error['message'] : 'unknown cause')); - } } /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey) { - return (function (string $key, string $variableKey) { - [,, $filePath] = $this->getFilePaths($key); - if (!is_file($filePath)) { - return null; - } + [,, $filePath] = $this->getFilePaths($key); - $cacheItem = require $filePath; + return (static function () use ($variableKey, $filePath) { + $cacheItem = @include $filePath; if (!$cacheItem instanceof CacheItem) { return null; } @@ -60,29 +59,34 @@ public function load(string $key, string $variableKey) } return $cacheItem->getData(); - })($key, $variableKey); + })(); } /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void + * @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(); + $exported = @var_export(new CacheItem($variableKey, $data), true); + $errorAfter = error_get_last(); + if ($errorAfter !== null && $errorBefore !== $errorAfter) { + throw new ShouldNotHappenException(sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message'])); + } 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 1723cd1f74..324dfcf37f 100644 --- a/src/Cache/MemoryCacheStorage.php +++ b/src/Cache/MemoryCacheStorage.php @@ -2,15 +2,15 @@ namespace PHPStan\Cache; -class MemoryCacheStorage implements CacheStorage +use function var_export; + +final class MemoryCacheStorage implements CacheStorage { - /** @var array */ + /** @var array */ private array $storage = []; /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey) @@ -28,14 +28,13 @@ public function load(string $key, string $variableKey) } /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void { - $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 5fd3845d1d..81793c6ba0 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -2,65 +2,44 @@ namespace PHPStan\Command; -use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; -use PHPStan\Analyser\ResultCache\ResultCacheManager; -use PHPStan\Parallel\ParallelAnalyser; -use PHPStan\Parallel\Scheduler; +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 file_exists; - -class AnalyseApplication +use function array_merge; +use function count; +use function is_file; +use function memory_get_peak_usage; +use function microtime; +use function sha1_file; +use function sprintf; + +/** + * @phpstan-import-type CollectorData from CollectedData + */ +final class AnalyseApplication { - private \PHPStan\Analyser\Analyser $analyser; - - private \PHPStan\PhpDoc\StubValidator $stubValidator; - - private ParallelAnalyser $parallelAnalyser; - - private Scheduler $scheduler; - - private \PHPStan\Analyser\ResultCache\ResultCacheManager $resultCacheManager; - - private IgnoredErrorHelper $ignoredErrorHelper; - - private string $memoryLimitFile; - - private int $internalErrorsCountLimit; - public function __construct( - Analyser $analyser, - StubValidator $stubValidator, - ParallelAnalyser $parallelAnalyser, - Scheduler $scheduler, - ResultCacheManager $resultCacheManager, - IgnoredErrorHelper $ignoredErrorHelper, - string $memoryLimitFile, - int $internalErrorsCountLimit + private AnalyserRunner $analyserRunner, + private AnalyserResultFinalizer $analyserResultFinalizer, + private StubValidator $stubValidator, + private ResultCacheManagerFactory $resultCacheManagerFactory, + private IgnoredErrorHelper $ignoredErrorHelper, + private StubFilesProvider $stubFilesProvider, ) { - $this->analyser = $analyser; - $this->stubValidator = $stubValidator; - $this->parallelAnalyser = $parallelAnalyser; - $this->scheduler = $scheduler; - $this->resultCacheManager = $resultCacheManager; - $this->ignoredErrorHelper = $ignoredErrorHelper; - $this->memoryLimitFile = $memoryLimitFile; - $this->internalErrorsCountLimit = $internalErrorsCountLimit; } /** * @param string[] $files - * @param bool $onlyFiles - * @param \PHPStan\Command\Output $stdOutput - * @param \PHPStan\Command\Output $errorOutput - * @param bool $defaultLevelUsed - * @param bool $debug - * @param string|null $projectConfigFile - * @return AnalysisResult + * @param mixed[]|null $projectConfigArray */ public function analyse( array $files, @@ -70,35 +49,27 @@ public function analyse( bool $defaultLevelUsed, bool $debug, ?string $projectConfigFile, - InputInterface $input + ?array $projectConfigArray, + InputInterface $input, ): AnalysisResult { - $this->updateMemoryLimitFile(); - $stubErrors = $this->stubValidator->validate(); - - register_shutdown_function(function (): void { - $error = error_get_last(); - if ($error === null) { - return; - } - if ($error['type'] !== E_ERROR) { - return; - } - - if (strpos($error['message'], 'Allowed memory size') !== false) { - return; - } - - @unlink($this->memoryLimitFile); - }); + $isResultCacheUsed = false; + $resultCacheManager = $this->resultCacheManagerFactory->create(); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + $fileSpecificErrors = []; if (count($ignoredErrorHelperResult->getErrors()) > 0) { - $errors = $ignoredErrorHelperResult->getErrors(); - $warnings = []; - $hasInternalErrors = false; + $notFileSpecificErrors = $ignoredErrorHelperResult->getErrors(); + $internalErrors = []; + $collectedData = []; + $savedResultCache = false; + $memoryUsageBytes = memory_get_peak_usage(true); + if ($errorOutput->isVeryVerbose()) { + $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); + } + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { - $resultCache = $this->resultCacheManager->restore($files, $debug); + $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); $intermediateAnalyserResult = $this->runAnalyser( $resultCache->getFilesToAnalyse(), $files, @@ -106,42 +77,109 @@ public function analyse( $projectConfigFile, $stdOutput, $errorOutput, - $input + $input, ); - $analyserResult = $this->resultCacheManager->process($intermediateAnalyserResult, $resultCache); - $internalErrors = $analyserResult->getInternalErrors(); - $errors = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $files, count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit()); - $warnings = $ignoredErrorHelperResult->getWarnings(); - $hasInternalErrors = count($internalErrors) > 0; - if ($analyserResult->hasReachedInternalErrorsCountLimit()) { - $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); - } - $errors = array_merge($errors, $internalErrors); - } - $errors = array_merge($stubErrors, $errors); + $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->getUnorderedErrors(), $stubErrors), + $intermediateAnalyserResult->getFilteredPhpErrors(), + $intermediateAnalyserResult->getAllPhpErrors(), + $intermediateAnalyserResult->getLocallyIgnoredErrors(), + $intermediateAnalyserResult->getLinesToIgnore(), + $intermediateAnalyserResult->getUnmatchedLineIgnores(), + $intermediateAnalyserResult->getInternalErrors(), + $intermediateAnalyserResult->getCollectedData(), + $intermediateAnalyserResult->getDependencies(), + $intermediateAnalyserResult->getExportedNodes(), + $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), + $intermediateAnalyserResult->getPeakMemoryUsageBytes(), + ); + } - $fileSpecificErrors = []; - $notFileSpecificErrors = []; - foreach ($errors as $error) { - if (is_string($error)) { - $notFileSpecificErrors[] = $error; - continue; + $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); + $analyserResult = $this->analyserResultFinalizer->finalize($resultCacheResult->getAnalyserResult(), $onlyFiles, $debug)->getAnalyserResult(); + $internalErrors = $analyserResult->getInternalErrors(); + $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( $fileSpecificErrors, $notFileSpecificErrors, - $warnings, + $internalErrors, + [], + $this->mapCollectedData($collectedData), $defaultLevelUsed, $projectConfigFile, - $hasInternalErrors + $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 @@ -153,7 +191,7 @@ private function runAnalyser( ?string $projectConfigFile, Output $stdOutput, Output $errorOutput, - InputInterface $input + InputInterface $input, ): AnalyserResult { $filesCount = count($files); @@ -162,112 +200,45 @@ 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)); } - /** @var bool $runningInParallel */ - $runningInParallel = false; - if (!$debug) { - $progressStarted = false; - $fileOrder = 0; $preFileCallback = null; - $postFileCallback = function (int $step) use ($errorOutput, &$progressStarted, $allAnalysedFilesCount, $filesCount, &$fileOrder, &$runningInParallel): 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); - - if ($runningInParallel) { - return; - } - - if ($fileOrder >= 100) { - $this->updateMemoryLimitFile(); - $fileOrder = 0; - } - $fileOrder += $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, &$startTime): void { + if ($startTime === null) { + throw new ShouldNotHappenException(); + } + $currentTotalMemory = memory_get_peak_usage(true); + $elapsedTime = microtime(true) - $startTime; + $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), $elapsedTime)); + $previousMemory = $currentTotalMemory; + }; + } } - // todo what about hyperthreading? should I divide CPU cores by 2? - $schedule = $this->scheduler->scheduleWork($this->getNumberOfCpuCores(), $files); - $mainScript = null; - if (isset($_SERVER['argv'][0]) && file_exists($_SERVER['argv'][0])) { - $mainScript = $_SERVER['argv'][0]; - } - - if ( - !$debug - && $mainScript !== null - && $schedule->getNumberOfProcesses() > 1 - ) { - $runningInParallel = true; - $analyserResult = $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $input); - } else { - $analyserResult = $this->analyser->analyse( - $files, - $preFileCallback, - $postFileCallback, - $debug, - $allAnalysedFiles - ); - } + $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $input); - if (isset($progressStarted) && $progressStarted) { + if (!$debug) { $errorOutput->getStyle()->progressFinish(); } return $analyserResult; } - private function updateMemoryLimitFile(): void - { - $bytes = memory_get_peak_usage(true); - $megabytes = ceil($bytes / 1024 / 1024); - file_put_contents($this->memoryLimitFile, sprintf('%d MB', $megabytes)); - } - - private function getNumberOfCpuCores(): int - { - // from brianium/paratest - $cores = 2; - 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 count($matches[0]); - } - } - - if (\DIRECTORY_SEPARATOR === '\\') { - // Windows - $process = @popen('wmic cpu get NumberOfLogicalProcessors', 'rb'); - if ($process !== false) { - fgets($process); - $cores = (int) fgets($process); - pclose($process); - } - - return $cores; - } - - $process = @\popen('sysctl -n hw.ncpu', 'rb'); - if ($process !== false) { - // *nix (Linux, BSD and Mac) - $cores = (int) fgets($process); - pclose($process); - } - - return $cores; - } - } diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 2d23579feb..a81115e108 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -2,21 +2,67 @@ namespace PHPStan\Command; +use OndraM\CiDetector\CiDetector; +use PHPStan\Analyser\InternalError; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; +use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; 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; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\StringInput; 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_file; +use function is_string; +use function pathinfo; +use function rewind; +use function sprintf; +use function str_contains; use function stream_get_contents; - -class AnalyseCommand extends \Symfony\Component\Console\Command\Command +use function strlen; +use function substr; +use const PATHINFO_BASENAME; +use const PATHINFO_EXTENSION; + +/** + * @phpstan-import-type Trace from InternalError as InternalErrorTrace + */ +final class AnalyseCommand extends Command { private const NAME = 'analyse'; @@ -25,18 +71,15 @@ class AnalyseCommand extends \Symfony\Component\Console\Command\Command public const DEFAULT_LEVEL = CommandHelper::DEFAULT_LEVEL; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, + private float $analysisStartTime, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -45,16 +88,20 @@ protected function configure(): void ->setDescription('Analyses source code') ->setDefinition([ new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(self::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption(ErrorsConsoleStyle::OPTION_NO_PROGRESS, null, InputOption::VALUE_NONE, 'Do not show progress bar, only results'), new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('error-format', null, InputOption::VALUE_REQUIRED, 'Format in which to print the result of the analysis', 'table'), - new InputOption('generate-baseline', null, InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), + new InputOption('error-format', null, InputOption::VALUE_REQUIRED, 'Format in which to print the result of the analysis', null), + 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'), ]); } @@ -71,7 +118,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v if ((bool) $input->getOption('debug')) { $application = $this->getApplication(); if ($application === null) { - throw new \PHPStan\ShouldNotHappenException(); + return; } $application->setCatchExceptions(false); return; @@ -85,9 +132,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(self::OPTION_LEVEL); - $pathsFile = $input->getOption('paths-file'); $allowXdebug = $input->getOption('xdebug'); $debugEnabled = (bool) $input->getOption('debug'); + $fix = (bool) $input->getOption('fix') || (bool) $input->getOption('watch') || (bool) $input->getOption('pro'); + $failWithoutResultCache = (bool) $input->getOption('fail-without-result-cache'); /** @var string|false|null $generateBaselineFile */ $generateBaselineFile = $input->getOption('generate-baseline'); @@ -97,16 +145,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generateBaselineFile = 'phpstan-baseline.neon'; } + $allowEmptyBaseline = (bool) $input->getOption('allow-empty-baseline'); + if ( !is_array($paths) || (!is_string($memoryLimit) && $memoryLimit !== null) || (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) - || (!is_string($pathsFile) && $pathsFile !== null) || (!is_bool($allowXdebug)) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } try { @@ -114,7 +163,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -122,18 +170,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generateBaselineFile, $level, $allowXdebug, + $debugEnabled, true, - $debugEnabled ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException $e) { return 1; } + if ($generateBaselineFile === null && $allowEmptyBaseline) { + $inceptionResult->getStdOutput()->getStyle()->error('You must pass the --generate-baseline option alongside --allow-empty-baseline.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + $errorOutput = $inceptionResult->getErrorOutput(); $errorFormat = $input->getOption('error-format'); if (!is_string($errorFormat) && $errorFormat !== null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + if ($errorFormat === null) { + $errorFormat = $inceptionResult->getContainer()->getParameter('errorFormat'); + } + + if ($errorFormat === null) { + $errorFormat = 'table'; } $container = $inceptionResult->getContainer(); @@ -142,150 +203,517 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorOutput->writeLineFormatted(sprintf( 'Error formatter "%s" not found. Available error formatters are: %s', $errorFormat, - implode(', ', array_map(static function (string $name): string { - return substr($name, strlen('errorFormatter.')); - }, $container->findServiceNamesByType(ErrorFormatter::class))) + implode(', ', array_map(static fn (string $name): string => substr($name, strlen('errorFormatter.')), $container->findServiceNamesByType(ErrorFormatter::class))), )); return 1; } - if ($errorFormat === 'baselineNeon') { - $errorOutput = $inceptionResult->getErrorOutput(); - $errorOutput->writeLineFormatted('⚠️ You\'re using an obsolete option --error-format baselineNeon. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(' There\'s a new and much better option --generate-baseline. Here are the advantages:'); - $errorOutput->writeLineFormatted(' 1) The current baseline file does not have to be commented-out'); - $errorOutput->writeLineFormatted(' nor emptied when generating the new baseline. It\'s excluded automatically.'); - $errorOutput->writeLineFormatted(' 2) Output no longer has to be redirected to a file, PHPStan saves the baseline'); - $errorOutput->writeLineFormatted(' to a specified path (defaults to phpstan-baseline.neon).'); - $errorOutput->writeLineFormatted(' 3) Baseline contains correct relative paths if saved to a subdirectory.'); - $errorOutput->writeLineFormatted(''); + $generateBaselineFile = $inceptionResult->getGenerateBaselineFile(); + if ($generateBaselineFile !== null) { + $baselineExtension = pathinfo($generateBaselineFile, PATHINFO_EXTENSION); + if ($baselineExtension === '') { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename must have an extension, %s provided instead.', pathinfo($generateBaselineFile, PATHINFO_BASENAME))); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + + if (!in_array($baselineExtension, ['neon', 'php'], true)) { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon or .php, .%s was used instead.', $baselineExtension)); + + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } } - /** @var ErrorFormatter $errorFormatter */ - $errorFormatter = $container->getService($errorFormatterServiceName); + 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; + } + + 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); + } - /** @var AnalyseApplication $application */ + return $this->runFixer($inceptionResult, $container, $onlyFiles, $input, $output, $files); + } + + /** @var AnalyseApplication $application */ $application = $container->getByType(AnalyseApplication::class); $debug = $input->getOption('debug'); if (!is_bool($debug)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $generateBaselineFile = $inceptionResult->getGenerateBaselineFile(); - if ($generateBaselineFile !== null) { - $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); + try { + $analysisResult = $application->analyse( + $files, + $onlyFiles, + $inceptionResult->getStdOutput(), + $inceptionResult->getErrorOutput(), + $inceptionResult->isDefaultLevelUsed(), + $debug, + $inceptionResult->getProjectConfigFile(), + $inceptionResult->getProjectConfigArray(), + $input, + ); + } catch (Throwable $t) { + if ($debug) { + $stdOutput = $inceptionResult->getStdOutput(); + $stdOutput->writeRaw(sprintf( + 'Uncaught %s: %s in %s:%d', + get_class($t), + $t->getMessage(), + $t->getFile(), + $t->getLine(), + )); + $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, null, $this->analysisStartTime); + } + + throw $t; + } + + /** + * 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; } - if ($baselineExtension !== 'neon') { - $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon, .%s was used instead.', $baselineExtension)); + $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; + } - return $inceptionResult->handleReturn(1); + if (!$hasStackTrace) { + if (!array_key_exists($fileSpecificError->getMessage(), $internalFileSpecificErrors)) { + $internalFileSpecificErrors[$fileSpecificError->getMessage()] = $fileSpecificError; + } } + + $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]; } - $analysisResult = $application->analyse( - $inceptionResult->getFiles(), - $inceptionResult->isOnlyFiles(), - $inceptionResult->getStdOutput(), - $inceptionResult->getErrorOutput(), - $inceptionResult->isDefaultLevelUsed(), - $debug, - $inceptionResult->getProjectConfigFile(), - $input - ); + $internalErrorsTuples = array_values($internalErrorsTuples); + + $fileHelper = $container->getByType(FileHelper::class); + + /** + * Variable $internalErrors only contains non-file-specific "internal errors". + */ + $internalErrors = []; + foreach ($internalErrorsTuples as [$internalError, $isInFileSpecificErrors]) { + if ($isInFileSpecificErrors) { + continue; + } + + $internalErrors[] = new InternalError( + $this->getMessageFromInternalError($fileHelper, $internalError, $output->getVerbosity()), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ); + } if ($generateBaselineFile !== null) { - if (!$analysisResult->hasErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + if (count($internalErrorsTuples) > 0) { + foreach ($internalErrorsTuples as [$internalError]) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + } + + $inceptionResult->getStdOutput()->getStyle()->error(sprintf( + '%s occurred. Baseline could not be generated.', + count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', + )); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } - if ($analysisResult->hasInternalErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see what\'s going on.'); - return $inceptionResult->handleReturn(1); + return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache); + } + + /** @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)); + } + + $errorOutput->writeLineFormatted(''); + + $errorOutput->writeLineFormatted('When you edit them and re-run PHPStan, the result cache will get stale.'); + + $directoriesToAdd = []; + foreach ($projectServiceFileNamesNotInAnalysedPaths as $path) { + $directoriesToAdd[] = dirname($relativePathHelper->getRelativePath($path)); + } + + $directoriesToAdd = array_unique($directoriesToAdd); + $oneDirectory = count($directoriesToAdd) === 1; + + $errorOutput->writeLineFormatted(sprintf('Add %s to your analysed paths to get rid of this problem:', $oneDirectory ? 'this directory' : 'these directories')); + + $errorOutput->writeLineFormatted(''); + + foreach ($directoriesToAdd as $directory) { + $errorOutput->writeLineFormatted(sprintf('- %s', $directory)); + } + + $errorOutput->writeLineFormatted(''); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } + } - $baselineFileDirectory = dirname($generateBaselineFile); - $baselineErrorFormatter = new BaselineNeonErrorFormatter(new ParentDirectoryRelativePathHelper($baselineFileDirectory)); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); - $streamOutput = $this->createStreamOutput(); - $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); - $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); - $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + 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); + } - $stream = $streamOutput->getStream(); - rewind($stream); - $baselineContents = stream_get_contents($stream); - if ($baselineContents === false) { - throw new \PHPStan\ShouldNotHappenException(); + 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; } - if (!is_dir($baselineFileDirectory)) { - $mkdirResult = @mkdir($baselineFileDirectory, 0644, true); - if ($mkdirResult === false) { - $inceptionResult->getStdOutput()->writeLineFormatted(sprintf('Failed to create directory "%s".', $baselineFileDirectory)); + $file = $fileHelper->normalizePath($traceItem['file'], '/'); - return $inceptionResult->handleReturn(1); - } + if (str_contains($file, '/larastan/')) { + $hasLarastan = true; + $isLaravelLast = false; + continue; } - try { - FileWriter::write($generateBaselineFile, $baselineContents); - } catch (\PHPStan\File\CouldNotWriteFileException $e) { - $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); + if (!str_contains($file, '/laravel/framework/')) { + continue; + } - return $inceptionResult->handleReturn(1); + $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; } - $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; + $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(); - $errorsCount++; + 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"); + } } + } - $message = sprintf('Baseline generated with %d %s.', $errorsCount, $errorsCount === 1 ? 'error' : 'errors'); + return $message; + } - if ( - $unignorableCount === 0 - && count($analysisResult->getNotFileSpecificErrors()) === 0 - ) { - $inceptionResult->getStdOutput()->getStyle()->success($message); + private function generateBaseline(string $generateBaselineFile, InceptionResult $inceptionResult, AnalysisResult $analysisResult, OutputInterface $output, bool $allowEmptyBaseline, string $baselineExtension, bool $failWithoutResultCache): int + { + if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) { + $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); + $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } + + $streamOutput = $this->createStreamOutput(); + $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); + $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); + $baselineFileDirectory = dirname($generateBaselineFile); + $baselinePathHelper = new ParentDirectoryRelativePathHelper($baselineFileDirectory); + + if ($baselineExtension === 'php') { + $baselineErrorFormatter = new BaselinePhpErrorFormatter($baselinePathHelper); + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + } else { + $baselineErrorFormatter = new BaselineNeonErrorFormatter($baselinePathHelper); + $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); + } + + $stream = $streamOutput->getStream(); + rewind($stream); + $baselineContents = stream_get_contents($stream); + if ($baselineContents === false) { + throw new ShouldNotHappenException(); + } + + try { + DirectoryCreator::ensureDirectoryExists($baselineFileDirectory, 0644); + } catch (DirectoryCreatorException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } + + try { + FileWriter::write($generateBaselineFile, $baselineContents); + } catch (CouldNotWriteFileException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); + + 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 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'); + + 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 and fix them."); + $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan with \"-vv\" and fix them."); } + } - return $inceptionResult->handleReturn(0); + $exitCode = 0; + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; } - return $inceptionResult->handleReturn( - $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()) + return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } + + /** + * @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 FixerApplication $fixerApplication */ + $fixerApplication = $container->getByType(FixerApplication::class); + + 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 \PHPStan\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 new file mode 100644 index 0000000000..5b88529382 --- /dev/null +++ b/src/Command/AnalyserRunner.php @@ -0,0 +1,88 @@ +scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $files); + $mainScript = null; + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $mainScript = $_SERVER['argv'][0]; + } + + if ( + !$debug + && $allowParallel + && function_exists('proc_open') + && $mainScript !== null + && $schedule->getNumberOfProcesses() > 0 + ) { + $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( + $files, + $preFileCallback, + $postFileCallback, + $debug, + $allAnalysedFiles, + ); + } + +} diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index bec01c8d3f..1697b5f38a 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -3,63 +3,56 @@ namespace PHPStan\Command; use PHPStan\Analyser\Error; - -class AnalysisResult +use PHPStan\Analyser\InternalError; +use PHPStan\Collectors\CollectedData; +use function count; +use function usort; + +/** + * @api + */ +final class AnalysisResult { - /** @var \PHPStan\Analyser\Error[] sorted by their file name, line number and message */ + /** @var list sorted by their file name, line number and message */ private array $fileSpecificErrors; - /** @var string[] */ - private array $notFileSpecificErrors; - - /** @var string[] */ - private array $warnings; - - private bool $defaultLevelUsed; - - private ?string $projectConfigFile; - - private bool $hasInternalErrors; - /** - * @param \PHPStan\Analyser\Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - * @param string[] $warnings - * @param bool $defaultLevelUsed - * @param string|null $projectConfigFile - * @param bool $hasInternalErrors + * @param list $fileSpecificErrors + * @param list $notFileSpecificErrors + * @param list $internalErrors + * @param list $warnings + * @param list $collectedData + * @param array $changedProjectExtensionFilesOutsideOfAnalysedPaths */ public function __construct( array $fileSpecificErrors, - array $notFileSpecificErrors, - array $warnings, - bool $defaultLevelUsed, - ?string $projectConfigFile, - bool $hasInternalErrors + 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( $fileSpecificErrors, - static function (Error $a, Error $b): int { - return [ - $a->getFile(), - $a->getLine(), - $a->getMessage(), - ] <=> [ - $b->getFile(), - $b->getLine(), - $b->getMessage(), - ]; - } + static fn (Error $a, Error $b): int => [ + $a->getFile(), + $a->getLine(), + $a->getMessage(), + ] <=> [ + $b->getFile(), + $b->getLine(), + $b->getMessage(), + ], ); $this->fileSpecificErrors = $fileSpecificErrors; - $this->notFileSpecificErrors = $notFileSpecificErrors; - $this->warnings = $warnings; - $this->defaultLevelUsed = $defaultLevelUsed; - $this->projectConfigFile = $projectConfigFile; - $this->hasInternalErrors = $hasInternalErrors; } public function hasErrors(): bool @@ -73,7 +66,7 @@ public function getTotalErrorsCount(): int } /** - * @return \PHPStan\Analyser\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 { @@ -81,7 +74,7 @@ public function getFileSpecificErrors(): array } /** - * @return string[] + * @return list */ public function getNotFileSpecificErrors(): array { @@ -89,7 +82,15 @@ public function getNotFileSpecificErrors(): array } /** - * @return string[] + * @return list + */ + public function getInternalErrorObjects(): array + { + return $this->internalErrors; + } + + /** + * @return list */ public function getWarnings(): array { @@ -101,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; @@ -113,7 +122,30 @@ public function getProjectConfigFile(): ?string public function hasInternalErrors(): bool { - return $this->hasInternalErrors; + return count($this->internalErrors) > 0; + } + + 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 2d79e822c2..ac41019e2b 100644 --- a/src/Command/ClearResultCacheCommand.php +++ b/src/Command/ClearResultCacheCommand.php @@ -2,29 +2,28 @@ namespace PHPStan\Command; -use PHPStan\Analyser\ResultCache\ResultCacheManager; +use PHPStan\Analyser\ResultCache\ResultCacheClearer; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function is_bool; +use function is_string; -class ClearResultCacheCommand extends Command +final class ClearResultCacheCommand extends Command { private const NAME = 'clear-result-cache'; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -34,45 +33,64 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } try { $inceptionResult = CommandHelper::begin( $input, $output, - ['.'], - null, - null, + [], + $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, $configuration, null, '0', - false, - false + $allowXdebug, + $debugEnabled, + true, ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { return 1; } $container = $inceptionResult->getContainer(); - /** @var ResultCacheManager $resultCacheManager */ - $resultCacheManager = $container->getByType(ResultCacheManager::class); - $path = $resultCacheManager->clear(); + $resultCacheClearer = $container->getByType(ResultCacheClearer::class); + $path = $resultCacheClearer->clear(); $output->writeln('Result cache cleared from directory:'); $output->writeln($path); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 8df92453bc..a142ecdca8 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,40 +2,87 @@ namespace PHPStan\Command; +use Composer\Semver\Semver; use Composer\XdebugHandler\XdebugHandler; -use Nette\DI\Config\Adapters\PhpAdapter; use Nette\DI\Helpers; -use Nette\Schema\Context as SchemaContext; -use Nette\Schema\Processor; +use Nette\DI\InvalidConfigurationException; +use Nette\DI\ServiceCreationException; +use Nette\FileNotFoundException; +use Nette\InvalidStateException; +use Nette\Schema\ValidationException; +use Nette\Utils\AssertionException; use Nette\Utils\Strings; -use Nette\Utils\Validators; +use PHPStan\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\FileReader; +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; - -class CommandHelper +use Throwable; +use function array_filter; +use function array_key_exists; +use function array_map; +use function array_values; +use function class_exists; +use function count; +use function dirname; +use function error_get_last; +use function get_class; +use function getcwd; +use function getenv; +use function gettype; +use function implode; +use function ini_get; +use function ini_set; +use function is_dir; +use function is_file; +use function is_readable; +use function is_string; +use function register_shutdown_function; +use function spl_autoload_functions; +use function sprintf; +use function str_contains; +use function str_repeat; +use function sys_get_temp_dir; +use const DIRECTORY_SEPARATOR; +use const E_ERROR; +use const PHP_VERSION_ID; + +final class CommandHelper { public const DEFAULT_LEVEL = '0'; + private static ?string $reservedMemory = null; + /** * @param string[] $paths * @param string[] $composerAutoloaderProjectPaths + * + * @throws InceptionNotSuccessfulException */ public static function begin( InputInterface $input, OutputInterface $output, array $paths, - ?string $pathsFile, ?string $memoryLimit, ?string $autoloadFile, array $composerAutoloaderProjectPaths, @@ -43,44 +90,89 @@ public static function begin( ?string $generateBaselineFile, ?string $level, bool $allowXdebug, - bool $manageMemoryLimitFile = true, - bool $debugEnabled = false + bool $debugEnabled, + bool $cleanupContainerCache, ): InceptionResult { - if (!$allowXdebug) { - $xdebug = new XdebugHandler('phpstan', '--ansi'); - $xdebug->check(); - unset($xdebug); - } $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); - /** @var \PHPStan\Command\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 (!$allowXdebug) { + $xdebug = new XdebugHandler('phpstan'); + $xdebug->setPersistent(); + $xdebug->check(); + unset($xdebug); + } + + if ($allowXdebug) { + if (!XdebugHandler::isXdebugActive()) { + $errorOutput->getStyle()->note('You are running with "--xdebug" enabled, but the Xdebug PHP extension is not active. The process will not halt at breakpoints.'); + } else { + $errorOutput->getStyle()->note("You are running with \"--xdebug\" enabled, and the Xdebug PHP extension is active.\nThe process will halt at breakpoints, but PHPStan will run much slower.\nUse this only if you are debugging PHPStan itself or your custom extensions."); + } + } elseif (XdebugHandler::isXdebugActive()) { + $errorOutput->getStyle()->note('The Xdebug PHP extension is active, but "--xdebug" is not used. This may slow down performance and the process will not halt at breakpoints.'); + } elseif ($debugEnabled) { + $v = XdebugHandler::getSkippedVersion(); + if ($v !== '') { + $errorOutput->getStyle()->note( + "The Xdebug PHP extension is active, but \"--xdebug\" is not used.\n" . + "The process was restarted and it will not halt at breakpoints.\n" . + 'Use "--xdebug" if you want to halt at breakpoints.', + ); + } + } + if ($memoryLimit !== null) { - if (\Nette\Utils\Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { + if (Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { $errorOutput->writeLineFormatted(sprintf('Invalid memory limit format "%s".', $memoryLimit)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } if (ini_set('memory_limit', $memoryLimit) === false) { $errorOutput->writeLineFormatted(sprintf('Memory limit "%s" cannot be set.', $memoryLimit)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } } + self::$reservedMemory = str_repeat('PHPStan', 1463); // reserve 10 kB of space + register_shutdown_function(static function () use ($errorOutput): void { + self::$reservedMemory = null; + $error = error_get_last(); + if ($error === null) { + return; + } + if ($error['type'] !== E_ERROR) { + return; + } + + if (!str_contains($error['message'], 'Allowed memory size')) { + return; + } + + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted(sprintf('PHPStan process crashed because it reached configured PHP memory limit: %s', ini_get('memory_limit'))); + $errorOutput->writeLineFormatted('Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.'); + }); + $currentWorkingDirectory = getcwd(); if ($currentWorkingDirectory === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $currentWorkingDirectoryFileHelper = new FileHelper($currentWorkingDirectory); $currentWorkingDirectory = $currentWorkingDirectoryFileHelper->getWorkingDirectory(); + + /** @var list|false $autoloadFunctionsBefore */ + $autoloadFunctionsBefore = spl_autoload_functions(); + if ($autoloadFile !== null) { $autoloadFile = $currentWorkingDirectoryFileHelper->absolutizePath($autoloadFile); if (!is_file($autoloadFile)) { $errorOutput->writeLineFormatted(sprintf('Autoload file "%s" not found.', $autoloadFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } (static function (string $file): void { @@ -88,7 +180,15 @@ public static function begin( })($autoloadFile); } if ($projectConfigFile === null) { - foreach (['phpstan.neon', 'phpstan.neon.dist'] 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; @@ -110,58 +210,37 @@ public static function begin( $defaultLevelUsed = true; } - $paths = array_map(static function (string $path) use ($currentWorkingDirectoryFileHelper): string { - return $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($path)); - }, $paths); - - if (count($paths) === 0 && $pathsFile !== null) { - $pathsFile = $currentWorkingDirectoryFileHelper->absolutizePath($pathsFile); - if (!file_exists($pathsFile)) { - $errorOutput->writeLineFormatted(sprintf('Paths file %s does not exist.', $pathsFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - try { - $pathsString = FileReader::read($pathsFile); - } catch (\PHPStan\File\CouldNotReadFileException $e) { - $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - $paths = array_values(array_filter(explode("\n", $pathsString), static function (string $path): bool { - return trim($path) !== ''; - })); - - $pathsFileFileHelper = new FileHelper(dirname($pathsFile)); - $paths = array_map(static function (string $path) use ($pathsFileFileHelper): string { - return $pathsFileFileHelper->normalizePath($pathsFileFileHelper->absolutizePath($path)); - }, $paths); - } + $paths = array_map(static fn (string $path): string => $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($path)), $paths); $analysedPathsFromConfig = []; $containerFactory = new ContainerFactory($currentWorkingDirectory); + if ($cleanupContainerCache) { + $containerFactory->setJournalContainer(); + } + $projectConfig = null; if ($projectConfigFile !== null) { if (!is_file($projectConfigFile)) { $errorOutput->writeLineFormatted(sprintf('Project config file at path %s does not exist.', $projectConfigFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $loader = (new LoaderFactory( $currentWorkingDirectoryFileHelper, $containerFactory->getRootDirectory(), $containerFactory->getCurrentWorkingDirectory(), - $generateBaselineFile + $generateBaselineFile, ))->createLoader(); try { $projectConfig = $loader->load($projectConfigFile, null); - } catch (\Nette\InvalidStateException | \Nette\FileNotFoundException $e) { + } catch (InvalidStateException | FileNotFoundException $e) { $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $defaultParameters = [ 'rootDir' => $containerFactory->getRootDirectory(), 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), + 'env' => getenv(), ]; if (isset($projectConfig['parameters']['tmpDir'])) { @@ -170,8 +249,10 @@ public static function begin( if ($level === null && isset($projectConfig['parameters']['level'])) { $level = (string) $projectConfig['parameters']['level']; } - if (count($paths) === 0 && isset($projectConfig['parameters']['paths'])) { + if (isset($projectConfig['parameters']['paths'])) { $analysedPathsFromConfig = Helpers::expand($projectConfig['parameters']['paths'], $defaultParameters); + } + if (count($paths) === 0) { $paths = $analysedPathsFromConfig; } } @@ -181,181 +262,255 @@ public static function begin( $levelConfigFile = sprintf('%s/config.level%s.neon', $containerFactory->getConfigDirectory(), $level); if (!is_file($levelConfigFile)) { $errorOutput->writeLineFormatted(sprintf('Level config file %s was not found.', $levelConfigFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $additionalConfigFiles[] = $levelConfigFile; } if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { - foreach (\PHPStan\ExtensionInstaller\GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { + $generatedConfigReflection = new ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); + $generatedConfigDirectory = dirname($generatedConfigReflection->getFileName()); + foreach (GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { foreach ($extensionConfig['extra']['includes'] ?? [] as $includedFile) { if (!is_string($includedFile)) { $errorOutput->writeLineFormatted(sprintf('Cannot include config from package %s, expecting string file path but got %s', $name, gettype($includedFile))); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); + } + $includedFilePath = null; + if (isset($extensionConfig['relative_install_path'])) { + $includedFilePath = sprintf('%s/%s/%s', $generatedConfigDirectory, $extensionConfig['relative_install_path'], $includedFile); + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { + $includedFilePath = null; + } } - $includedFilePath = sprintf('%s/%s', $extensionConfig['install_path'], $includedFile); - if (!file_exists($includedFilePath) || !is_readable($includedFilePath)) { - $errorOutput->writeLineFormatted(sprintf('Config file %s does not exists or isn\'t readable', $includedFilePath)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + + if ($includedFilePath === null) { + $includedFilePath = sprintf('%s/%s', $extensionConfig['install_path'], $includedFile); + } + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { + $errorOutput->writeLineFormatted(sprintf('Config file %s does not exist or isn\'t readable', $includedFilePath)); + throw new InceptionNotSuccessfulException(); } $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 ($projectConfigFile !== null) { + if ( + $projectConfigFile !== null + && $currentWorkingDirectoryFileHelper->normalizePath($projectConfigFile, '/') !== $currentWorkingDirectoryFileHelper->normalizePath(__DIR__ . '/../../conf/config.stubFiles.neon', '/') + ) { $additionalConfigFiles[] = $projectConfigFile; } - $loaderParameters = [ - 'rootDir' => $containerFactory->getRootDirectory(), - 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), - ]; - - self::detectDuplicateIncludedFiles( - $errorOutput, - $currentWorkingDirectoryFileHelper, - $additionalConfigFiles, - $loaderParameters - ); + $createDir = static function (string $path) use ($errorOutput): void { + try { + DirectoryCreator::ensureDirectoryExists($path, 0777); + } catch (DirectoryCreatorException $e) { + $errorOutput->writeLineFormatted($e->getMessage()); + throw new InceptionNotSuccessfulException(); + } + }; if (!isset($tmpDir)) { $tmpDir = sys_get_temp_dir() . '/phpstan'; - if (!@mkdir($tmpDir, 0777) && !is_dir($tmpDir)) { - $errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $tmpDir)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - } - - if ($projectConfigFile !== null) { - $allCustomConfigFiles = self::getConfigFiles( - $currentWorkingDirectoryFileHelper, - new NeonAdapter(), - new PhpAdapter(), - $projectConfigFile, - $loaderParameters, - $generateBaselineFile - ); - } else { - $allCustomConfigFiles = []; + $createDir($tmpDir); } try { - $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $allCustomConfigFiles, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile); - } catch (\Nette\DI\InvalidConfigurationException | \Nette\Utils\AssertionException $e) { + $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()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - if (count($paths) === 0) { - $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } + throw new InceptionNotSuccessfulException(); + } catch (InvalidIgnoredErrorPatternsException $e) { + $errorOutput->writeLineFormatted(sprintf('Invalid %s in ignoreErrors:', count($e->getErrors()) === 1 ? 'entry' : 'entries')); + foreach ($e->getErrors() as $error) { + $errorOutput->writeLineFormatted($error); + $errorOutput->writeLineFormatted(''); + } - $memoryLimitFile = $container->getParameter('memoryLimitFile'); - if ($manageMemoryLimitFile && file_exists($memoryLimitFile)) { - $memoryLimitFileContents = FileReader::read($memoryLimitFile); - $errorOutput->writeLineFormatted('PHPStan crashed in the previous run probably because of excessive memory consumption.'); - $errorOutput->writeLineFormatted(sprintf('It consumed around %s of memory.', $memoryLimitFileContents)); - $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('To ignore non-existent paths in ignoreErrors,'); + $errorOutput->writeLineFormatted('set reportUnmatchedIgnoredErrors: false in your configuration file.'); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('To avoid this issue, allow to use more memory with the --memory-limit option.'); - @unlink($memoryLimitFile); - } - self::setUpSignalHandler($errorOutput, $manageMemoryLimitFile ? $memoryLimitFile : null); - if (!$container->hasParameter('customRulesetUsed')) { - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('No rules detected'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('You have the following choices:'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('* while running the analyse option, use the --level option to adjust your rule level - the higher the stricter'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(sprintf('* create your own custom ruleset by selecting which rules you want to check by copying the service definitions from the built-in config level files in %s.', $currentWorkingDirectoryFileHelper->normalizePath(__DIR__ . '/../../conf'))); - $errorOutput->writeLineFormatted(' * in this case, don\'t forget to define parameter customRulesetUsed in your config file.'); - $errorOutput->writeLineFormatted(''); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } elseif ((bool) $container->getParameter('customRulesetUsed')) { - $defaultLevelUsed = false; - } + 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(''); + } - $schema = $container->getParameter('__parametersSchema'); - $processor = new Processor(); - $processor->onNewContext[] = static function (SchemaContext $context): void { - $context->path = ['parameters']; - }; + $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(''); + } - try { - $processor->process($schema, $container->getParameters()); - } catch (\Nette\Schema\ValidationException $e) { + throw new InceptionNotSuccessfulException(); + } catch (ValidationException $e) { foreach ($e->getMessages() as $message) { $errorOutput->writeLineFormatted('Invalid configuration:'); $errorOutput->writeLineFormatted($message); } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } + throw new InceptionNotSuccessfulException(); + } catch (ServiceCreationException $e) { + $matches = Strings::match($e->getMessage(), '#Service of type (?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]): Service of type (?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]) needed by \$(?[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*) in (?[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)\(\)#'); + if ($matches === null) { + throw $e; + } + + if ($matches['parserServiceType'] !== 'PHPStan\\Parser\\Parser') { + throw $e; + } + + if ($matches['methodName'] !== '__construct') { + throw $e; + } + + $errorOutput->writeLineFormatted('Invalid configuration:'); + $errorOutput->writeLineFormatted(sprintf("Service of type %s is no longer autowired.\n", $matches['parserServiceType'])); + $errorOutput->writeLineFormatted('You need to choose one of the following services'); + $errorOutput->writeLineFormatted(sprintf('and use it in the %s argument of your service %s:', $matches['parameterName'], $matches['serviceType'])); + $errorOutput->writeLineFormatted('* defaultAnalysisParser (if you\'re parsing files from analysed paths)'); + $errorOutput->writeLineFormatted('* currentPhpVersionSimpleDirectParser (in most other situations)'); - $autoloadFiles = $container->getParameter('autoload_files'); - if ($manageMemoryLimitFile && count($autoloadFiles) > 0) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option autoload_files. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('You might not need it anymore - try removing it from your'); - $errorOutput->writeLineFormatted('configuration file and run PHPStan again.'); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('If the analysis fails, there are now two distinct options'); - $errorOutput->writeLineFormatted('to choose from to replace autoload_files:'); - $errorOutput->writeLineFormatted('1) scanFiles - PHPStan will scan those for classes and functions'); - $errorOutput->writeLineFormatted(' definitions. PHPStan will not execute those files.'); - $errorOutput->writeLineFormatted('2) bootstrapFiles - PHPStan will execute these files to prepare'); - $errorOutput->writeLineFormatted(' the PHP runtime environment for the analysis.'); + $errorOutput->writeLineFormatted('After fixing this problem, your configuration will look something like this:'); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('Read more about this in PHPStan\'s documentation:'); - $errorOutput->writeLineFormatted('https://phpstan.org/user-guide/discovering-symbols'); + $errorOutput->writeLineFormatted('-'); + $errorOutput->writeLineFormatted(sprintf("\tclass: %s", $matches['serviceType'])); + $errorOutput->writeLineFormatted(sprintf("\targuments:")); + $errorOutput->writeLineFormatted(sprintf("\t\t%s: @defaultAnalysisParser", $matches['parameterName'])); $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 (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.'); + } + + throw new InceptionNotSuccessfulException(); + } + + if ($cleanupContainerCache) { + $cacheStorage = $container->getService('cacheStorage'); + if ($cacheStorage instanceof FileCacheStorage) { + $cacheStorage->clearUnusedFiles(); + } } - $autoloadDirectories = $container->getParameter('autoload_directories'); - if (count($autoloadDirectories) > 0 && $manageMemoryLimitFile) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option autoload_directories. ⚠️️'); + /** @var bool|null $customRulesetUsed */ + $customRulesetUsed = $container->getParameter('customRulesetUsed'); + if ($customRulesetUsed === null) { $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('You might not need it anymore - try removing it from your'); - $errorOutput->writeLineFormatted('configuration file and run PHPStan again.'); + $errorOutput->writeLineFormatted('No rules detected'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('You have the following choices:'); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('If the analysis fails, replace it with scanDirectories.'); + $errorOutput->writeLineFormatted('* while running the analyse option, use the --level option to adjust your rule level - the higher the stricter'); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('Read more about this in PHPStan\'s documentation:'); - $errorOutput->writeLineFormatted('https://phpstan.org/user-guide/discovering-symbols'); + $errorOutput->writeLineFormatted(sprintf('* create your own custom ruleset by selecting which rules you want to check by copying the service definitions from the built-in config level files in %s.', $currentWorkingDirectoryFileHelper->normalizePath(__DIR__ . '/../../conf'))); + $errorOutput->writeLineFormatted(' * in this case, don\'t forget to define parameter customRulesetUsed in your config file.'); $errorOutput->writeLineFormatted(''); + throw new InceptionNotSuccessfulException(); + } elseif ($customRulesetUsed) { + $defaultLevelUsed = false; } - foreach ($autoloadFiles as $parameterAutoloadFile) { - if (!file_exists($parameterAutoloadFile)) { - $errorOutput->writeLineFormatted(sprintf('Autoload file %s does not exist.', $parameterAutoloadFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - (static function (string $file) use ($container): void { - require_once $file; - })($parameterAutoloadFile); + foreach ($container->getParameter('bootstrapFiles') as $bootstrapFileFromArray) { + self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled); } - $bootstrapFile = $container->getParameter('bootstrap'); - if ($bootstrapFile !== null) { - if ($manageMemoryLimitFile) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option bootstrap. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('This option has been replaced with bootstrapFiles which accepts a list of files'); - $errorOutput->writeLineFormatted('to execute before the analysis.'); - $errorOutput->writeLineFormatted(''); + /** @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; } - self::executeBootstrapFile($bootstrapFile, $container, $errorOutput, $debugEnabled); + $GLOBALS['__phpstanAutoloadFunctions'] = $newAutoloadFunctions; } - foreach ($container->getParameter('bootstrapFiles') as $bootstrapFileFromArray) { - self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled); + if (PHP_VERSION_ID >= 80000) { + require_once __DIR__ . '/../../stubs/runtime/Enum/UnitEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/BackedEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; } foreach ($container->getParameter('scanFiles') as $scannedFile) { @@ -365,7 +520,7 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Scanned file %s does not exist.', $scannedFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } foreach ($container->getParameter('scanDirectories') as $scannedDirectory) { @@ -375,166 +530,91 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Scanned directory %s does not exist.', $scannedDirectory)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); + } + + $alreadyAddedStubFiles = []; + foreach ($container->getParameter('stubFiles') as $stubFile) { + if (array_key_exists($stubFile, $alreadyAddedStubFiles)) { + $errorOutput->writeLineFormatted(sprintf('Stub file %s is added multiple times.', $stubFile)); + + throw new InceptionNotSuccessfulException(); + } + + $alreadyAddedStubFiles[$stubFile] = true; + + if (is_file($stubFile)) { + continue; + } + + $errorOutput->writeLineFormatted(sprintf('Stub file %s does not exist.', $stubFile)); + + throw new InceptionNotSuccessfulException(); } /** @var FileFinder $fileFinder */ - $fileFinder = $container->getByType(FileFinder::class); + $fileFinder = $container->getService('fileFinderAnalyse'); - try { + $pathRoutingParser = $container->getService('pathRoutingParser'); + + $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); - } catch (\PHPStan\File\PathNotFoundException $e) { - $errorOutput->writeLineFormatted(sprintf('%s', $e->getMessage())); - throw new \PHPStan\Command\InceptionNotSuccessfulException($e->getMessage(), 0, $e); - } + $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()]; + }; return new InceptionResult( - $fileFinderResult->getFiles(), - $fileFinderResult->isOnlyFiles(), + $filesCallback, $stdOutput, $errorOutput, $container, $defaultLevelUsed, - $memoryLimitFile, $projectConfigFile, - $generateBaselineFile + $projectConfig, + $generateBaselineFile, ); } + /** + * @throws InceptionNotSuccessfulException + */ private static function executeBootstrapFile( string $file, Container $container, Output $errorOutput, - bool $debugEnabled + bool $debugEnabled, ): void { if (!is_file($file)) { $errorOutput->writeLineFormatted(sprintf('Bootstrap file %s does not exist.', $file)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } try { (static function (string $file) use ($container): void { require_once $file; })($file); - } catch (\Throwable $e) { + } catch (Throwable $e) { $errorOutput->writeLineFormatted(sprintf('%s thrown in %s on line %d while loading bootstrap file %s: %s', get_class($e), $e->getFile(), $e->getLine(), $file, $e->getMessage())); if ($debugEnabled) { $errorOutput->writeLineFormatted($e->getTraceAsString()); } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - } - - private static function setUpSignalHandler(Output $output, ?string $memoryLimitFile): void - { - if (!function_exists('pcntl_signal')) { - return; - } - - pcntl_async_signals(true); - pcntl_signal(SIGINT, static function () use ($output, $memoryLimitFile): void { - if ($memoryLimitFile !== null && file_exists($memoryLimitFile)) { - @unlink($memoryLimitFile); - } - $output->writeLineFormatted(''); - exit(1); - }); - } - - /** - * @param \PHPStan\Command\Output $output - * @param \PHPStan\File\FileHelper $fileHelper - * @param string[] $configFiles - * @param array $loaderParameters - * @throws \PHPStan\Command\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 function (string $file) use ($fileHelper): string { - return $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(); } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - /** - * @param \PHPStan\DependencyInjection\NeonAdapter $neonAdapter - * @param \Nette\DI\Config\Adapters\PhpAdapter $phpAdapter - * @param string $configFile - * @param array $loaderParameters - * @param string|null $generateBaselineFile - * @return string[] - */ - private static function getConfigFiles( - 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 (Strings::endsWith($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/DumpDependenciesCommand.php b/src/Command/DumpDependenciesCommand.php deleted file mode 100644 index 2305f58394..0000000000 --- a/src/Command/DumpDependenciesCommand.php +++ /dev/null @@ -1,117 +0,0 @@ -composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - } - - protected function configure(): void - { - $this->setName(self::NAME) - ->setDescription('Dumps files dependency tree') - ->setDefinition([ - new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run dump on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), - new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), - new InputOption(ErrorsConsoleStyle::OPTION_NO_PROGRESS, null, InputOption::VALUE_NONE, 'Do not show progress bar, only results'), - new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), - new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for the run'), - new InputOption('analysed-paths', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Project-scope paths'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), - ]); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - try { - /** @var string[] $paths */ - $paths = $input->getArgument('paths'); - - /** @var string|null $memoryLimit */ - $memoryLimit = $input->getOption('memory-limit'); - - /** @var string|null $autoloadFile */ - $autoloadFile = $input->getOption('autoload-file'); - - /** @var string|null $configurationFile */ - $configurationFile = $input->getOption('configuration'); - - /** @var string|null $pathsFile */ - $pathsFile = $input->getOption('paths-file'); - - /** @var bool $allowXdebug */ - $allowXdebug = $input->getOption('xdebug'); - - $inceptionResult = CommandHelper::begin( - $input, - $output, - $paths, - $pathsFile, - $memoryLimit, - $autoloadFile, - $this->composerAutoloaderProjectPaths, - $configurationFile, - null, - '0', // irrelevant but prevents an error when a config file is passed - $allowXdebug, - true - ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { - return 1; - } - - $stdOutput = $inceptionResult->getStdOutput(); - $stdOutputStyole = $stdOutput->getStyle(); - - /** @var DependencyDumper $dependencyDumper */ - $dependencyDumper = $inceptionResult->getContainer()->getByType(DependencyDumper::class); - - /** @var FileHelper $fileHelper */ - $fileHelper = $inceptionResult->getContainer()->getByType(FileHelper::class); - - /** @var string[] $analysedPaths */ - $analysedPaths = $input->getOption('analysed-paths'); - $analysedPaths = array_map(static function (string $path) use ($fileHelper): string { - return $fileHelper->absolutizePath($path); - }, $analysedPaths); - $dependencies = $dependencyDumper->dumpDependencies( - $inceptionResult->getFiles(), - static function (int $count) use ($stdOutputStyole): void { - $stdOutputStyole->progressStart($count); - }, - static function () use ($stdOutputStyole): void { - $stdOutputStyole->progressAdvance(); - }, - count($analysedPaths) > 0 ? $analysedPaths : null - ); - $stdOutputStyole->progressFinish(); - $stdOutput->writeLineFormatted(Json::encode($dependencies, Json::PRETTY)); - - return $inceptionResult->handleReturn(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 b82765fd42..ac02e1f9e1 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -2,33 +2,34 @@ namespace PHPStan\Command\ErrorFormatter; +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 { - private \PHPStan\File\RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, + string $existingBaselineContent, ): int { if (!$analysisResult->hasErrors()) { - $output->writeRaw(Neon::encode([ - 'parameters' => [ - 'ignoreErrors' => [], - ], - ], Neon::BLOCK)); + $output->writeRaw($this->getNeon([], $existingBaselineContent)); return 0; } @@ -37,37 +38,90 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors[$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; + } + + $fileErrorsByMessage[$errorMessage][0]++; + + if ($identifier === null) { continue; } - $fileErrorsCounts[$errorMessage]++; + if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { + $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + continue; + } + + $fileErrorsByMessage[$errorMessage][1][$identifier]++; } + ksort($fileErrorsByMessage, SORT_STRING); - foreach ($fileErrorsCounts as $message => $count) { - $errorsToOutput[] = [ - 'message' => '#^' . preg_quote($message, '#') . '$#', - 'count' => $count, - 'path' => $this->relativePathHelper->getRelativePath($file), - ]; + 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 061d8439a5..9072bd8dd2 100644 --- a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php +++ b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php @@ -2,23 +2,26 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function count; +use function htmlspecialchars; +use function sprintf; +use const ENT_COMPAT; +use const ENT_XML1; -class CheckstyleErrorFormatter implements ErrorFormatter +final class CheckstyleErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { $output->writeRaw(''); @@ -29,15 +32,16 @@ public function formatErrors( foreach ($this->groupByFile($analysisResult) as $relativeFilePath => $errors) { $output->writeRaw(sprintf( '', - $this->escape($relativeFilePath) + $this->escape($relativeFilePath), )); $output->writeLineFormatted(''); foreach ($errors as $error) { $output->writeRaw(sprintf( - ' ', + ' ', $this->escape((string) $error->getLine()), - $this->escape((string) $error->getMessage()) + $this->escape($error->getMessage()), + $error->getIdentifier() !== null ? sprintf(' source="%s"', $this->escape($error->getIdentifier())) : '', )); $output->writeLineFormatted(''); } @@ -82,10 +86,8 @@ public function formatErrors( /** * Escapes values for using in XML * - * @param string $string - * @return string */ - protected function escape(string $string): string + private function escape(string $string): string { return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8'); } @@ -93,22 +95,21 @@ protected function escape(string $string): string /** * Group errors by file * - * @param AnalysisResult $analysisResult - * @return array Array that have as key the relative path of file - * and as value an array with occurred errors. + * @return array> Array that have as key the relative path of file + * and as value an array with occurred errors. */ private function groupByFile(AnalysisResult $analysisResult): array { $files = []; - /** @var \PHPStan\Analyser\Error $fileSpecificError */ + /** @var Error $fileSpecificError */ foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $absolutePath = $fileSpecificError->getFilePath(); if ($fileSpecificError->getTraitFilePath() !== null) { $absolutePath = $fileSpecificError->getTraitFilePath(); } $relativeFilePath = $this->relativePathHelper->getRelativePath( - $absolutePath + $absolutePath, ); $files[$relativeFilePath][] = $fileSpecificError; diff --git a/src/Command/ErrorFormatter/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 ab3831c733..2d4e17ee31 100644 --- a/src/Command/ErrorFormatter/ErrorFormatter.php +++ b/src/Command/ErrorFormatter/ErrorFormatter.php @@ -5,19 +5,31 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +/** + * 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 { /** * Formats the errors and outputs them to the console. * - * @param \PHPStan\Command\AnalysisResult $analysisResult - * @param \PHPStan\Command\Output $output * @return int Error code. */ public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int; } diff --git a/src/Command/ErrorFormatter/GithubErrorFormatter.php b/src/Command/ErrorFormatter/GithubErrorFormatter.php new file mode 100644 index 0000000000..21ffdd2650 --- /dev/null +++ b/src/Command/ErrorFormatter/GithubErrorFormatter.php @@ -0,0 +1,74 @@ +getFileSpecificErrors() as $fileSpecificError) { + $metas = [ + 'file' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), + 'line' => $fileSpecificError->getLine(), + 'col' => 0, + ]; + array_walk($metas, static function (&$value, string $key): void { + $value = sprintf('%s=%s', $key, (string) $value); + }); + + $message = $fileSpecificError->getMessage(); + // newlines need to be encoded + // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 + $message = str_replace("\n", '%0A', $message); + + $line = sprintf('::error %s::%s', implode(',', $metas), $message); + + $output->writeRaw($line); + $output->writeLineFormatted(''); + } + + foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { + // newlines need to be encoded + // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 + $notFileSpecificError = str_replace("\n", '%0A', $notFileSpecificError); + + $line = sprintf('::error ::%s', $notFileSpecificError); + + $output->writeRaw($line); + $output->writeLineFormatted(''); + } + + foreach ($analysisResult->getWarnings() as $warning) { + // newlines need to be encoded + // see https://github.com/actions/starter-workflows/issues/68#issuecomment-581479448 + $warning = str_replace("\n", '%0A', $warning); + + $line = sprintf('::warning ::%s', $warning); + + $output->writeRaw($line); + $output->writeLineFormatted(''); + } + + return $analysisResult->hasErrors() ? 1 : 0; + } + +} diff --git a/src/Command/ErrorFormatter/GitlabErrorFormatter.php b/src/Command/ErrorFormatter/GitlabErrorFormatter.php index 8d093247b1..9a8ccb35cd 100644 --- a/src/Command/ErrorFormatter/GitlabErrorFormatter.php +++ b/src/Command/ErrorFormatter/GitlabErrorFormatter.php @@ -6,18 +6,17 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function hash; +use function implode; /** * @see https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html#implementing-a-custom-tool */ -class GitlabErrorFormatter implements ErrorFormatter +final class GitlabErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int @@ -34,21 +33,18 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in $fileSpecificError->getFile(), $fileSpecificError->getLine(), $fileSpecificError->getMessage(), - ] - ) + ], + ), ), + 'severity' => $fileSpecificError->canBeIgnored() ? 'major' : 'blocker', 'location' => [ 'path' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), 'lines' => [ - 'begin' => $fileSpecificError->getLine(), + 'begin' => $fileSpecificError->getLine() ?? 0, ], ], ]; - if (!$fileSpecificError->canBeIgnored()) { - $error['severity'] = 'blocker'; - } - $errorsArray[] = $error; } @@ -56,6 +52,7 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in $errorsArray[] = [ 'description' => $notFileSpecificError, 'fingerprint' => hash('sha256', $notFileSpecificError), + 'severity' => 'major', 'location' => [ 'path' => '', 'lines' => [ diff --git a/src/Command/ErrorFormatter/JsonErrorFormatter.php b/src/Command/ErrorFormatter/JsonErrorFormatter.php index c2ea2f6f56..0a4174d4e0 100644 --- a/src/Command/ErrorFormatter/JsonErrorFormatter.php +++ b/src/Command/ErrorFormatter/JsonErrorFormatter.php @@ -5,15 +5,16 @@ use Nette\Utils\Json; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use stdClass; +use Symfony\Component\Console\Formatter\OutputFormatter; +use function count; +use function property_exists; -class JsonErrorFormatter implements ErrorFormatter +final class JsonErrorFormatter implements ErrorFormatter { - private bool $pretty; - - public function __construct(bool $pretty) + public function __construct(private bool $pretty) { - $this->pretty = $pretty; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int @@ -23,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 d05326b22b..59131dcf01 100644 --- a/src/Command/ErrorFormatter/JunitErrorFormatter.php +++ b/src/Command/ErrorFormatter/JunitErrorFormatter.php @@ -5,28 +5,31 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function htmlspecialchars; use function sprintf; +use const ENT_COMPAT; +use const ENT_XML1; -class JunitErrorFormatter implements ErrorFormatter +final class JunitErrorFormatter implements ErrorFormatter { - private \PHPStan\File\RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { + $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()) { @@ -60,10 +63,7 @@ public function formatErrors( /** * Format a single test case * - * @param string $reference - * @param string|null $message * - * @return string */ private function createTestCase(string $reference, string $type, ?string $message = null): string { @@ -81,10 +81,8 @@ private function createTestCase(string $reference, string $type, ?string $messag /** * Escapes values for using in XML * - * @param string $string - * @return string */ - protected function escape(string $string): string + private function escape(string $string): string { return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8'); } diff --git a/src/Command/ErrorFormatter/RawErrorFormatter.php b/src/Command/ErrorFormatter/RawErrorFormatter.php index 7d926c3b7c..f761a5a47e 100644 --- a/src/Command/ErrorFormatter/RawErrorFormatter.php +++ b/src/Command/ErrorFormatter/RawErrorFormatter.php @@ -4,13 +4,14 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use function sprintf; -class RawErrorFormatter implements ErrorFormatter +final class RawErrorFormatter implements ErrorFormatter { public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { @@ -18,14 +19,21 @@ 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() - ) + $fileSpecificError->getMessage(), + $identifier, + ), ); $output->writeLineFormatted(''); } diff --git a/src/Command/ErrorFormatter/TableErrorFormatter.php b/src/Command/ErrorFormatter/TableErrorFormatter.php index a02a7aa435..f69eca834e 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -2,32 +2,46 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; use PHPStan\Command\AnalyseCommand; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; - -class TableErrorFormatter implements ErrorFormatter +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; + +final class TableErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - private bool $showTipsOfTheDay; - public function __construct( - RelativePathHelper $relativePathHelper, - bool $showTipsOfTheDay + private RelativePathHelper $relativePathHelper, + private SimpleRelativePathHelper $simpleRelativePathHelper, + private CiDetectedErrorFormatter $ciDetectedErrorFormatter, + private bool $showTipsOfTheDay, + private ?string $editorUrl, + private ?string $editorUrlTitle, ) { - $this->relativePathHelper = $relativePathHelper; - $this->showTipsOfTheDay = $showTipsOfTheDay; } + /** @api */ public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { + $this->ciDetectedErrorFormatter->formatErrors($analysisResult, $output); $projectConfigFile = 'phpstan.neon'; if ($analysisResult->getProjectConfigFile() !== null) { $projectConfigFile = $this->relativePathHelper->getRelativePath($analysisResult->getProjectConfigFile()); @@ -37,13 +51,14 @@ public function formatErrors( if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) { $style->success('No errors'); + if ($this->showTipsOfTheDay) { if ($analysisResult->isDefaultLevelUsed()) { $output->writeLineFormatted('💡 Tip of the Day:'); $output->writeLineFormatted(sprintf( "PHPStan is performing only the most basic checks.\nYou can pass a higher rule level through the --%s option\n(the default and current level is %d) to analyse code more thoroughly.", AnalyseCommand::OPTION_LEVEL, - AnalyseCommand::DEFAULT_LEVEL + AnalyseCommand::DEFAULT_LEVEL, )); $output->writeLineFormatted(''); } @@ -52,7 +67,7 @@ public function formatErrors( return 0; } - /** @var array $fileErrors */ + /** @var array $fileErrors */ $fileErrors = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { if (!isset($fileErrors[$fileSpecificError->getFile()])) { @@ -66,33 +81,69 @@ 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)) { + $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, ]; } - $relativeFilePath = $this->relativePathHelper->getRelativePath($file); - - $style->table(['Line', $relativeFilePath], $rows); + $style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows); } if (count($analysisResult->getNotFileSpecificErrors()) > 0) { - $style->table(['', 'Error'], array_map(static function (string $error): array { - return ['', $error]; - }, $analysisResult->getNotFileSpecificErrors())); + $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', OutputFormatter::escape($error)], $analysisResult->getNotFileSpecificErrors())); } $warningsCount = count($analysisResult->getWarnings()); if ($warningsCount > 0) { - $style->table(['', 'Warning'], array_map(static function (string $warning): array { - return ['', $warning]; - }, $analysisResult->getWarnings())); + $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', OutputFormatter::escape($warning)], $analysisResult->getWarnings())); } $finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount()); @@ -109,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 new file mode 100644 index 0000000000..070896a051 --- /dev/null +++ b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php @@ -0,0 +1,123 @@ +getFileSpecificErrors(); + $notFileSpecificErrors = $analysisResult->getNotFileSpecificErrors(); + $warnings = $analysisResult->getWarnings(); + + if (count($fileSpecificErrors) === 0 && count($notFileSpecificErrors) === 0 && count($warnings) === 0) { + return 0; + } + + $result .= $this->createTeamcityLine('inspectionType', [ + 'id' => 'phpstan', + 'name' => 'phpstan', + 'category' => 'phpstan', + 'description' => 'phpstan Inspection', + ]); + + 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' => $message, + 'file' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), + 'line' => $fileSpecificError->getLine(), + // additional attributes + 'SEVERITY' => 'ERROR', + 'ignorable' => $fileSpecificError->canBeIgnored(), + 'tip' => $fileSpecificError->getTip(), + ]); + } + + foreach ($notFileSpecificErrors as $notFileSpecificError) { + $result .= $this->createTeamcityLine('inspection', [ + 'typeId' => 'phpstan', + 'message' => $notFileSpecificError, + // the file is required + 'file' => $analysisResult->getProjectConfigFile() !== null ? $this->relativePathHelper->getRelativePath($analysisResult->getProjectConfigFile()) : '.', + 'SEVERITY' => 'ERROR', + ]); + } + + foreach ($warnings as $warning) { + $result .= $this->createTeamcityLine('inspection', [ + 'typeId' => 'phpstan', + 'message' => $warning, + // the file is required + 'file' => $analysisResult->getProjectConfigFile() !== null ? $this->relativePathHelper->getRelativePath($analysisResult->getProjectConfigFile()) : '.', + 'SEVERITY' => 'WARNING', + ]); + } + + $output->writeRaw($result); + + return $analysisResult->hasErrors() ? 1 : 0; + } + + /** + * Creates a Teamcity report line + * + * @param string $messageName The message name + * @param mixed[] $keyValuePairs The key=>value pairs + * @return string The Teamcity report line + */ + private function createTeamcityLine(string $messageName, array $keyValuePairs): string + { + $string = '##teamcity[' . $messageName; + foreach ($keyValuePairs as $key => $value) { + if (is_string($value)) { + $value = $this->escape($value); + } + $string .= ' ' . $key . '=\'' . $value . '\''; + } + return $string . ']' . PHP_EOL; + } + + /** + * Escapes the given string for Teamcity output + * + * @param string $string The string to escape + * @return string The escaped string + */ + private function escape(string $string): string + { + $replacements = [ + '~\n~' => '|n', + '~\r~' => '|r', + '~([\'\|\[\]])~' => '|$1', + ]; + return (string) preg_replace(array_keys($replacements), array_values($replacements), $string); + } + +} diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index 9596836e9c..f953d40c49 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -4,17 +4,28 @@ use OndraM\CiDetector\CiDetector; use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - -class ErrorsConsoleStyle extends \Symfony\Component\Console\Style\SymfonyStyle +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Console\Terminal; +use function array_unshift; +use function explode; +use function implode; +use function sprintf; +use function str_starts_with; +use function strlen; +use function wordwrap; +use const DIRECTORY_SEPARATOR; + +final class ErrorsConsoleStyle extends SymfonyStyle { public const OPTION_NO_PROGRESS = 'no-progress'; private bool $showProgress; - private \Symfony\Component\Console\Helper\ProgressBar $progressBar; + private ProgressBar $progressBar; private ?bool $isCiDetected = null; @@ -41,7 +52,7 @@ private function isCiDetected(): bool public function table(array $headers, array $rows): void { /** @var int $terminalWidth */ - $terminalWidth = (new \Symfony\Component\Console\Terminal())->getWidth() - 2; + $terminalWidth = (new Terminal())->getWidth() - 2; $maxHeaderWidth = strlen($headers[0]); foreach ($rows as $row) { $length = strlen($row[0]); @@ -52,42 +63,124 @@ public function table(array $headers, array $rows): void $maxHeaderWidth = $length; } - $wrap = static function ($rows) use ($terminalWidth, $maxHeaderWidth) { - return array_map(static function ($row) use ($terminalWidth, $maxHeaderWidth) { - return array_map(static function ($s) use ($terminalWidth, $maxHeaderWidth) { - if ($terminalWidth > $maxHeaderWidth + 5) { - return wordwrap( - $s, - $terminalWidth - $maxHeaderWidth - 5, - "\n", - true - ); - } + // manual wrapping could be replaced with $table->setColumnMaxWidth() + // but it's buggy for lines + // https://github.com/symfony/symfony/issues/45520 + // https://github.com/symfony/symfony/issues/45521 + $headers = $this->wrap($headers, $terminalWidth, $maxHeaderWidth); + foreach ($headers as $i => $header) { + $newHeader = []; + foreach (explode("\n", $header) as $h) { + $newHeader[] = sprintf('%s', $h); + } - return $s; - }, $row); - }, $rows); - }; + $headers[$i] = implode("\n", $newHeader); + } - parent::table($headers, $wrap($rows)); + foreach ($rows as $i => $row) { + $rows[$i] = $this->wrap($row, $terminalWidth, $maxHeaderWidth); + } + + $table = $this->createTable(); + array_unshift($rows, $headers, new TableSeparator()); + $table->setRows($rows); + + $table->render(); + $this->newLine(); } /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $max + * @param string[] $rows + * @return string[] */ - public function createProgressBar($max = 0): ProgressBar + private function wrap(array $rows, int $terminalWidth, int $maxHeaderWidth): array + { + foreach ($rows as $i => $column) { + $columnRows = explode("\n", $column); + foreach ($columnRows as $k => $columnRow) { + if (str_starts_with($columnRow, '✏️')) { + continue; + } + $wrapped = wordwrap( + $columnRow, + $terminalWidth - $maxHeaderWidth - 5, + ); + 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); + } + + return $rows; + } + + public function createProgressBar(int $max = 0): ProgressBar { $this->progressBar = parent::createProgressBar($max); - $this->progressBar->setOverwrite(!$this->isCiDetected()); + + $format = $this->getProgressBarFormat(); + if ($format !== null) { + $this->progressBar->setFormat($format); + } + + $ci = $this->isCiDetected(); + $this->progressBar->setOverwrite(!$ci); + + if ($ci) { + $this->progressBar->minSecondsBetweenRedraws(15); + $this->progressBar->maxSecondsBetweenRedraws(30); + } elseif (DIRECTORY_SEPARATOR === '\\') { + $this->progressBar->minSecondsBetweenRedraws(0.5); + $this->progressBar->maxSecondsBetweenRedraws(2); + } else { + $this->progressBar->minSecondsBetweenRedraws(0.1); + $this->progressBar->maxSecondsBetweenRedraws(0.5); + } + return $this->progressBar; } - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $max - */ - public function progressStart($max = 0): void + 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) { return; @@ -95,25 +188,12 @@ public function progressStart($max = 0): void parent::progressStart($max); } - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $step - */ - public function progressAdvance($step = 1): void + public function progressAdvance(int $step = 1): void { if (!$this->showProgress) { return; } - if (!$this->isCiDetected() && $step > 0) { - $stepTime = (time() - $this->progressBar->getStartTime()) / $step; - if ($stepTime > 0 && $stepTime < 1) { - $this->progressBar->setRedrawFrequency((int) (1 / $stepTime)); - } else { - $this->progressBar->setRedrawFrequency(1); - } - } - parent::progressAdvance($step); } diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php new file mode 100644 index 0000000000..1a9e64d7fe --- /dev/null +++ b/src/Command/FixerApplication.php @@ -0,0 +1,588 @@ +|null */ + private PromiseInterface|null $processInProgress = 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 IgnoredErrorHelper $ignoredErrorHelper, + private StubFilesProvider $stubFilesProvider, + private array $analysedPaths, + private string $currentWorkingDirectory, + private string $proTmpDir, + private array $dnsServers, + private array $composerAutoloaderProjectPaths, + private array $allConfigFiles, + private ?string $cliAutoloadFile, + private array $bootstrapFiles, + private ?string $editorUrl, + private string $usedLevel, + ) + { + } + + public function run( + ?string $projectConfigFile, + InputInterface $input, + OutputInterface $output, + int $filesCount, + string $mainScript, + ): int + { + $loop = new StreamSelectLoop(); + $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); + + $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' => [ + 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'analysedPaths' => $this->analysedPaths, + 'projectConfigFile' => $projectConfigFile, + 'filesCount' => $filesCount, + 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'editorUrl' => $this->editorUrl, + 'ruleLevel' => $this->usedLevel, + ]]); + $decoder->on('data', function (array $data) use ( + $output, + ): 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'] === 'resumeFileMonitor') { + $this->fileMonitorActive = true; + return; + } + if ($data['action'] === 'pauseFileMonitor') { + $this->fileMonitorActive = false; + return; + } + }); + + $this->fileMonitor->initialize(array_merge( + $this->analysedPaths, + $this->getComposerLocks(), + $this->getComposerInstalled(), + $this->getExecutedFiles(), + $this->getStubFiles(), + $this->allConfigFiles, + )); + + $this->analyse( + $loop, + $mainScript, + $projectConfigFile, + $input, + $output, + $encoder, + ); + + $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output): void { + if ($this->processInProgress !== null) { + $this->processInProgress->cancel(); + $this->processInProgress = null; + } + + if (count($changes->getChangedFiles()) > 0) { + $encoder->write(['action' => 'changedFiles', 'data' => [ + 'paths' => $changes->getChangedFiles(), + ]]); + } + + $this->analyse( + $loop, + $mainScript, + $projectConfigFile, + $input, + $output, + $encoder, + ); + }); + }); + + try { + $fixerProcess = $this->getFixerProcess($output, $serverPort); + } catch (FixerProcessException) { + return 1; + } + + $fixerProcess->start($loop); + $fixerProcess->on('exit', function ($exitCode) use ($output, $loop): void { + $loop->stop(); + if ($exitCode === null) { + return; + } + if ($exitCode === 0) { + return; + } + $output->writeln(sprintf('PHPStan Pro process exited with code %d.', $exitCode)); + @unlink($this->proTmpDir . '/phar-info.json'); + }); + + $loop->run(); + + return 0; + } + + /** + * @throws FixerProcessException + */ + private function getFixerProcess(OutputInterface $output, int $serverPort): Process + { + try { + DirectoryCreator::ensureDirectoryExists($this->proTmpDir, 0777); + } catch (DirectoryCreatorException $e) { + $output->writeln($e->getMessage()); + throw new FixerProcessException(); + } + + $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)) { + $this->printDownloadError($output, $e); + + throw new FixerProcessException(); + } + } + + $pubKeyPath = $pharPath . '.pubkey'; + FileWriter::write($pubKeyPath, FileReader::read(__DIR__ . '/fixer-phar.pubkey')); + + try { + $phar = new Phar($pharPath); + } 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(); + } + + if ($phar->getSignature()['hash_type'] !== 'OpenSSL') { + @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']; + $isDocker = $this->isDockerRunning(); + if ($isDocker) { + $output->writeln('Running in Docker? Don\'t forget to do these steps:'); + + $output->writeln('1) Publish this port when running Docker:'); + $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->proTmpDir)); + $output->writeln(''); + } + } else { + $isDocker = $this->isDockerRunning(); + if ($isDocker) { + $output->writeln('Running in Docker? You need to do these steps in order to launch PHPStan Pro:'); + $output->writeln(''); + $output->writeln('1) Set the PHPSTAN_PRO_WEB_PORT environment variable in the Dockerfile:'); + $output->writeln(' ENV PHPSTAN_PRO_WEB_PORT=11111'); + $output->writeln('2) Expose this port in the Dockerfile:'); + $output->writeln(' EXPOSE 11111'); + $output->writeln('3) Publish this port when running Docker:'); + $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->proTmpDir)); + $output->writeln(''); + } + } + + 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( + OutputInterface $output, + string $pharPath, + string $infoPath, + ): void + { + $currentVersion = null; + $branch = '2.0.x'; + if (is_file($pharPath) && is_file($infoPath)) { + /** @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 ( + $currentBranch === $branch + && (new DateTimeImmutable('', new DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours') + ) { + return; + } + + $output->writeln('Checking if there\'s a new PHPStan Pro release...'); + } + + $dnsConfig = new Config(); + $dnsConfig->nameservers = $this->dnsServers; + + $client = new Browser( + new Connector( + [ + 'timeout' => 5, + 'tls' => [ + 'cafile' => CaBundle::getBundledCaBundlePath(), + ], + 'dns' => $dnsConfig, + ], + ), + ); + + /** + * @var array{url: string, version: string} $latestInfo + */ + $latestInfo = Json::decode((string) await($client->get(sprintf('https://fixer-download-api.phpstan.com/latest?%s', http_build_query(['phpVersion' => PHP_VERSION_ID, 'branch' => $branch]))))->getBody(), Json::FORCE_ARRAY); + if ($currentVersion !== null && $latestInfo['version'] === $currentVersion) { + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); + $output->writeln('You\'re running the latest PHPStan Pro!'); + return; + } + + $output->writeln('Downloading the latest PHPStan Pro...'); + + $pharPathResource = fopen($pharPath, 'w'); + if ($pharPathResource === false) { + throw new ShouldNotHappenException(sprintf('Could not open file %s for writing.', $pharPath)); + } + $progressBar = new ProgressBar($output); + $client->requestStreaming('GET', $latestInfo['url'])->then(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { + $body = $response->getBody(); + if (!$body instanceof ReadableStreamInterface) { + throw new ShouldNotHappenException(); + } + + $totalSize = (int) $response->getHeaderLine('Content-Length'); + $progressBar->setFormat('file_download'); + $progressBar->setMessage(sprintf('%.2f MB', $totalSize / 1000000), 'fileSize'); + $progressBar->start($totalSize); + + $bytes = 0; + $body->on('data', static function ($chunk) use ($pharPathResource, $progressBar, &$bytes): void { + $bytes += strlen($chunk); + fwrite($pharPathResource, $chunk); + $progressBar->setProgress($bytes); + }); + }, function (Throwable $e) use ($output): void { + $this->printDownloadError($output, $e); + }); + + Loop::run(); + + fclose($pharPathResource); + + $progressBar->finish(); + $output->writeln(''); + $output->writeln(''); + + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); + } + + private function printDownloadError(OutputInterface $output, Throwable $e): void + { + $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + $output->writeln(''); + $output->writeln('Try different DNS servers in your configuration file:'); + $output->writeln(''); + $output->writeln('parameters:'); + $output->writeln("\tpro:"); + $output->writeln("\t\tdnsServers!:"); + $output->writeln("\t\t\t- '8.8.8.8'"); + $output->writeln(''); + } + + private function writeInfoFile(string $infoPath, string $version, string $branch): void + { + FileWriter::write($infoPath, Json::encode([ + 'version' => $version, + 'branch' => $branch, + 'date' => (new DateTimeImmutable('', new DateTimeZone('UTC')))->format(DateTime::ATOM), + ])); + } + + /** + * @param callable(FileMonitorResult): void $hasChangesCallback + */ + 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()) { + $hasChangesCallback($changes); + } + + $loop->addTimer(1.0, $callback); + }; + $loop->addTimer(1.0, $callback); + } + + private function analyse( + LoopInterface $loop, + string $mainScript, + ?string $projectConfigFile, + InputInterface $input, + OutputInterface $output, + Encoder $phpstanFixerEncoder, + ): void + { + $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); + + $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, + [ + '--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(); + + 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 isDockerRunning(): bool + { + 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; + } + + return $locks; + } + + /** + * @return list + */ + private function getComposerInstalled(): array + { + $files = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $composer = ComposerHelper::getComposerConfig($autoloadPath); + if ($composer === null) { + continue; + } + + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; + if (!is_file($filePath)) { + continue; + } + + $files[] = $filePath; + } + + return $files; + } + + /** + * @return list + */ + private function getExecutedFiles(): array + { + $files = []; + if ($this->cliAutoloadFile !== null) { + $files[] = $this->cliAutoloadFile; + } + + foreach ($this->bootstrapFiles as $bootstrapFile) { + $files[] = $bootstrapFile; + } + + return $files; + } + + /** + * @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 new file mode 100644 index 0000000000..c9e4097d58 --- /dev/null +++ b/src/Command/FixerProcessException.php @@ -0,0 +1,10 @@ +setName(self::NAME) + ->setDescription('(Internal) Support for PHPStan Pro.') + ->setDefinition([ + new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), + new InputOption('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('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('server-port', null, InputOption::VALUE_REQUIRED, 'Server port for FixerApplication'), + ]) + ->setHidden(true); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $paths = $input->getArgument('paths'); + $memoryLimit = $input->getOption('memory-limit'); + $autoloadFile = $input->getOption('autoload-file'); + $configuration = $input->getOption('configuration'); + $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + $allowXdebug = $input->getOption('xdebug'); + $serverPort = $input->getOption('server-port'); + + if ( + !is_array($paths) + || (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_string($autoloadFile) && $autoloadFile !== null) + || (!is_string($configuration) && $configuration !== null) + || (!is_string($level) && $level !== null) + || (!is_bool($allowXdebug)) + || (!is_string($serverPort)) + ) { + throw new ShouldNotHappenException(); + } + + try { + $inceptionResult = CommandHelper::begin( + $input, + $output, + $paths, + $memoryLimit, + $autoloadFile, + $this->composerAutoloaderProjectPaths, + $configuration, + null, + $level, + $allowXdebug, + false, + false, + ); + } catch (InceptionNotSuccessfulException) { + return 1; + } + + $container = $inceptionResult->getContainer(); + + /** @var IgnoredErrorHelper $ignoredErrorHelper */ + $ignoredErrorHelper = $container->getByType(IgnoredErrorHelper::class); + $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); + if (count($ignoredErrorHelperResult->getErrors()) > 0) { + throw new ShouldNotHappenException(); + } + + $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(); + } + + $out->write([ + 'action' => 'analysisStart', + 'result' => [ + 'analysedFiles' => $inceptionFiles, + ], + ]); + + $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); + + $errorsFromResultCacheTmp = $resultCache->getErrors(); + $locallyIgnoredErrorsFromResultCacheTmp = $resultCache->getLocallyIgnoredErrors(); + foreach ($resultCache->getFilesToAnalyse() as $fileToAnalyse) { + unset($errorsFromResultCacheTmp[$fileToAnalyse]); + unset($locallyIgnoredErrorsFromResultCacheTmp[$fileToAnalyse]); + } + + $errorsFromResultCache = []; + foreach ($errorsFromResultCacheTmp as $errorsByFile) { + foreach ($errorsByFile as $error) { + $errorsFromResultCache[] = $error; + } + } + + [$errorsFromResultCache, $ignoredErrorsFromResultCache] = $this->filterErrors($errorsFromResultCache, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); + + foreach ($locallyIgnoredErrorsFromResultCacheTmp as $locallyIgnoredErrors) { + foreach ($locallyIgnoredErrors as $locallyIgnoredError) { + $ignoredErrorsFromResultCache[] = [$locallyIgnoredError, null]; + } + } + + $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); + } + + 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; + } + + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); + } + + $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, + ), + ], + ]]); + } + + [$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); + } + + 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(), + ]; + } + + /** + * @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 $parallelAnalyser->analyse( + $loop, + $schedule, + $mainScript, + null, + $configuration, + $input, + $onFileAnalysisHandler, + ); + } + +} diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index 78dac29f5e..4340e0bd99 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -4,26 +4,25 @@ use Hoa\Compiler\Llk\Parser; use Hoa\Compiler\Llk\TreeNode; +use Hoa\Exception\Exception; use Nette\Utils\Strings; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\PhpDocParser\Parser\ParserException; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use function count; +use function strrpos; use function substr; -class IgnoredRegexValidator +final class IgnoredRegexValidator { - private Parser $parser; - - private \PHPStan\PhpDoc\TypeStringResolver $typeStringResolver; - public function __construct( - Parser $parser, - TypeStringResolver $typeStringResolver + private Parser $parser, + private TypeStringResolver $typeStringResolver, ) { - $this->parser = $parser; - $this->typeStringResolver = $typeStringResolver; } public function validate(string $regex): IgnoredRegexValidatorResult @@ -33,22 +32,25 @@ public function validate(string $regex): IgnoredRegexValidatorResult try { /** @var TreeNode $ast */ $ast = $this->parser->parse($regex); - } catch (\Hoa\Exception\Exception $e) { - if (strpos($e->getMessage(), 'Unexpected token "|" (alternation) 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), - false + false, ); } /** - * @param TreeNode $ast * @return array */ private function getIgnoredTypes(TreeNode $ast): array @@ -77,19 +79,21 @@ private function getIgnoredTypes(TreeNode $ast): array try { $type = $this->typeStringResolver->resolve($matches[1], null); - } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) { + } catch (ParserException) { 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; @@ -100,7 +104,7 @@ private function removeDelimiters(string $regex): string $delimiter = substr($regex, 0, 1); $endDelimiterPosition = strrpos($regex, $delimiter); if ($endDelimiterPosition === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return substr($regex, 1, $endDelimiterPosition - 1); diff --git a/src/Command/IgnoredRegexValidatorResult.php b/src/Command/IgnoredRegexValidatorResult.php index ddd3d6b71b..0906a3c84b 100644 --- a/src/Command/IgnoredRegexValidatorResult.php +++ b/src/Command/IgnoredRegexValidatorResult.php @@ -2,30 +2,20 @@ namespace PHPStan\Command; -class IgnoredRegexValidatorResult +final class IgnoredRegexValidatorResult { - /** @var array */ - private array $ignoredTypes; - - private bool $anchorsInTheMiddle; - - private bool $allErrorsIgnored; - /** * @param array $ignoredTypes - * @param bool $anchorsInTheMiddle - * @param bool $allErrorsIgnored */ public function __construct( - array $ignoredTypes, - bool $anchorsInTheMiddle, - bool $allErrorsIgnored + private array $ignoredTypes, + private bool $anchorsInTheMiddle, + private bool $allErrorsIgnored, + private ?string $wrongSequence = null, + private ?string $escapedWrongSequence = null, ) { - $this->ignoredTypes = $ignoredTypes; - $this->anchorsInTheMiddle = $anchorsInTheMiddle; - $this->allErrorsIgnored = $allErrorsIgnored; } /** @@ -46,4 +36,14 @@ public function areAllErrorsIgnored(): bool return $this->allErrorsIgnored; } + public function getWrongSequence(): ?string + { + return $this->wrongSequence; + } + + public function getEscapedWrongSequence(): ?string + { + return $this->escapedWrongSequence; + } + } diff --git a/src/Command/InceptionNotSuccessfulException.php b/src/Command/InceptionNotSuccessfulException.php index 872b71cfaa..5cbcf4425e 100644 --- a/src/Command/InceptionNotSuccessfulException.php +++ b/src/Command/InceptionNotSuccessfulException.php @@ -2,7 +2,9 @@ namespace PHPStan\Command; -class InceptionNotSuccessfulException extends \Exception +use Exception; + +final class InceptionNotSuccessfulException extends Exception { } diff --git a/src/Command/InceptionResult.php b/src/Command/InceptionResult.php index 4b821e7321..fc6056eccb 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -3,75 +3,51 @@ 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 string[] */ - private array $files; - - private bool $onlyFiles; - - private Output $stdOutput; - - private Output $errorOutput; - - private \PHPStan\DependencyInjection\Container $container; - - private bool $isDefaultLevelUsed; - - private string $memoryLimitFile; - - private ?string $projectConfigFile; - - private ?string $generateBaselineFile; + /** @var callable(): (array{string[], bool}) */ + private $filesCallback; /** - * @param string[] $files - * @param bool $onlyFiles - * @param Output $stdOutput - * @param Output $errorOutput - * @param \PHPStan\DependencyInjection\Container $container - * @param bool $isDefaultLevelUsed - * @param string $memoryLimitFile - * @param string|null $projectConfigFile - * @param string|null $generateBaselineFile + * @param callable(): (array{string[], bool}) $filesCallback + * @param mixed[]|null $projectConfigArray */ public function __construct( - array $files, - bool $onlyFiles, - Output $stdOutput, - Output $errorOutput, - Container $container, - bool $isDefaultLevelUsed, - string $memoryLimitFile, - ?string $projectConfigFile, - ?string $generateBaselineFile + callable $filesCallback, + private Output $stdOutput, + private Output $errorOutput, + private Container $container, + private bool $isDefaultLevelUsed, + private ?string $projectConfigFile, + private ?array $projectConfigArray, + private ?string $generateBaselineFile, ) { - $this->files = $files; - $this->onlyFiles = $onlyFiles; - $this->stdOutput = $stdOutput; - $this->errorOutput = $errorOutput; - $this->container = $container; - $this->isDefaultLevelUsed = $isDefaultLevelUsed; - $this->memoryLimitFile = $memoryLimitFile; - $this->projectConfigFile = $projectConfigFile; - $this->generateBaselineFile = $generateBaselineFile; + $this->filesCallback = $filesCallback; } /** - * @return string[] + * @throws InceptionNotSuccessfulException + * @throws PathNotFoundException + * @return array{string[], bool} */ public function getFiles(): array { - return $this->files; - } + $callback = $this->filesCallback; - public function isOnlyFiles(): bool - { - return $this->onlyFiles; + /** @throws InceptionNotSuccessfulException|PathNotFoundException */ + return $callback(); } public function getStdOutput(): Output @@ -99,37 +75,53 @@ public function getProjectConfigFile(): ?string return $this->projectConfigFile; } + /** + * @return mixed[]|null + */ + public function getProjectConfigArray(): ?array + { + return $this->projectConfigArray; + } + 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', $this->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)), + )); } - @unlink($this->memoryLimitFile); return $exitCode; } - private function bytes(int $bytes): string + private function formatDuration(int $seconds): string { - $bytes = round($bytes); - $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; - foreach ($units as $unit) { - if (abs($bytes) < 1024 || $unit === end($units)) { - break; - } - $bytes /= 1024; + $minutes = (int) floor($seconds / 60); + $remainingSeconds = $seconds % 60; + + $result = []; + if ($minutes > 0) { + $result[] = $minutes . ' minute' . ($minutes > 1 ? 's' : ''); } - if (!isset($unit)) { - throw new \PHPStan\ShouldNotHappenException(); + if ($remainingSeconds > 0) { + $result[] = $remainingSeconds . ' second' . ($remainingSeconds > 1 ? 's' : ''); } - return round($bytes, 2) . ' ' . $unit; + return implode(' ', $result); } } diff --git a/src/Command/Output.php b/src/Command/Output.php index 2c10c6f56f..b0efcd648a 100644 --- a/src/Command/Output.php +++ b/src/Command/Output.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +/** @api */ interface Output { @@ -15,4 +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/OutputStyle.php b/src/Command/OutputStyle.php index b97466f52d..ed3ffff606 100644 --- a/src/Command/OutputStyle.php +++ b/src/Command/OutputStyle.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +/** @api */ interface OutputStyle { diff --git a/src/Command/Symfony/SymfonyOutput.php b/src/Command/Symfony/SymfonyOutput.php index a53d40b8e6..2d66f11a38 100644 --- a/src/Command/Symfony/SymfonyOutput.php +++ b/src/Command/Symfony/SymfonyOutput.php @@ -9,20 +9,14 @@ /** * @internal */ -class SymfonyOutput implements Output +final class SymfonyOutput implements Output { - private \Symfony\Component\Console\Output\OutputInterface $symfonyOutput; - - private OutputStyle $style; - public function __construct( - OutputInterface $symfonyOutput, - OutputStyle $style + private OutputInterface $symfonyOutput, + private OutputStyle $style, ) { - $this->symfonyOutput = $symfonyOutput; - $this->style = $style; } public function writeFormatted(string $message): void @@ -50,4 +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 ba2f25ee16..e8782a5f59 100644 --- a/src/Command/Symfony/SymfonyStyle.php +++ b/src/Command/Symfony/SymfonyStyle.php @@ -8,17 +8,14 @@ /** * @internal */ -class SymfonyStyle implements OutputStyle +final class SymfonyStyle implements OutputStyle { - private \Symfony\Component\Console\Style\StyleInterface $symfonyStyle; - - public function __construct(StyleInterface $symfonyStyle) + public function __construct(private StyleInterface $symfonyStyle) { - $this->symfonyStyle = $symfonyStyle; } - public function getSymfonyStyle(): \Symfony\Component\Console\Style\StyleInterface + public function getSymfonyStyle(): StyleInterface { return $this->symfonyStyle; } diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index 9275126488..27fdb1ce80 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -4,11 +4,14 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; -use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\DependencyInjection\Container; -use PHPStan\Rules\Registry; +use PHPStan\File\PathNotFoundException; +use PHPStan\Rules\Registry as RuleRegistry; +use PHPStan\ShouldNotHappenException; use React\EventLoop\StreamSelectLoop; use React\Socket\ConnectionInterface; use React\Socket\TcpConnector; @@ -19,24 +22,31 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; +use function array_fill_keys; +use function array_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'; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; + private int $errorCount = 0; /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -45,15 +55,15 @@ protected function configure(): void ->setDescription('(Internal) Support for parallel analysis.') ->setDefinition([ new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), 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), - ]); + ]) + ->setHidden(true); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -63,7 +73,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); - $pathsFile = $input->getOption('paths-file'); $allowXdebug = $input->getOption('xdebug'); $port = $input->getOption('port'); $identifier = $input->getOption('identifier'); @@ -74,12 +83,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) - || (!is_string($pathsFile) && $pathsFile !== null) || (!is_bool($allowXdebug)) || !is_string($port) || !is_string($identifier) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } try { @@ -87,7 +95,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -95,71 +102,97 @@ protected function execute(InputInterface $input, OutputInterface $output): int null, $level, $allowXdebug, - false + false, + false, ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException $e) { return 1; } $loop = new StreamSelectLoop(); $container = $inceptionResult->getContainer(); - $analysedFiles = $inceptionResult->getFiles(); + try { + [$analysedFiles] = $inceptionResult->getFiles(); + } 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))->then(function (ConnectionInterface $connection) use ($container, $identifier, $analysedFiles): void { - $out = new Encoder($connection); - $in = new Decoder($connection, true, 512, 0, $container->getParameter('parallel')['buffer']); + $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, $analysedFiles); + $this->runWorker($container, $out, $in, $output, $analysedFiles); }); $loop->run(); + if ($this->errorCount > 0) { + return 1; + } + return 0; } /** - * @param Container $container - * @param WritableStreamInterface $out - * @param ReadableStreamInterface $in * @param array $analysedFiles */ private function runWorker( Container $container, WritableStreamInterface $out, ReadableStreamInterface $in, - array $analysedFiles + OutputInterface $output, + array $analysedFiles, ): void { - $handleError = static function (\Throwable $error) use ($out): void { + $handleError = function (Throwable $error) use ($out, $output): void { + $this->errorCount++; + $output->writeln(sprintf('Error: %s', $error->getMessage())); $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); - - // todo collectErrors (from Analyser) - $in->on('data', static function (array $json) use ($fileAnalyser, $registry, $out, $analysedFiles): 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; @@ -168,25 +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 { - $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; } - } catch (\Throwable $t) { + 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) { $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' + $internalErrors[] = new InternalError( + $t->getMessage(), + sprintf('analysing file %s', $file), + InternalError::prepareTrace($t), + $t->getTraceAsString(), + true, ); - $errors[] = new Error($internalErrorMessage, $file, null, false); } } @@ -194,8 +249,17 @@ 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, - 'filesCount' => count($files), + 'exportedNodes' => $exportedNodes, + 'files' => $files, 'internalErrorsCount' => $internalErrorsCount, ]]); }); diff --git a/src/Command/fixer-phar.pubkey b/src/Command/fixer-phar.pubkey new file mode 100644 index 0000000000..4792ce5af8 --- /dev/null +++ b/src/Command/fixer-phar.pubkey @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAs+kHoHhs66QaDeVmF6zb +kqgEafJTHVsKraOJp64zLlrIZhEiBjKnl5EwpIvvPOfgjjI9z9zm0Y8IoWMfNMf1 +MN/9f7603vfJxTAdXVVPtTq4x3dKf1z8RdE9+04Nrfb/+OuAVCtDGCjKec9sRdce +4Ex499BfQx80njFoeC84eY39hNf82rhflW9OMCQeEZhv3durZV5q+tgp+2pdf0yw +sdIc/NYebZB1C0Cj6AfqbT9WoMAojfG8R5tF+4S3mMiDHNXx6hNM4mJEpyODje1A +qZW91T0x1rFWe25WWLtQG/VP1E+an03C8axn3Ag7+9gohE5hNRKfOWZLZsx+KivD +sivJSiyZEP6h6Mxp3aVYk9fmxyJnn0+tvPGYm3wZlPYp0SQIMeYooPr1ddERwtxm +4TyoQ6v8tg+7hrPu4I5km7X8uUzKFtLWj5CB+DVQycjzSbA3Wrj7EcXbKlx8hoyl +onQWCVNkSY75CuizY3YqGJr1lCYH7Gut/IAD+gT7CuqgU0PrsSE0c1yBI36Xz090 +XZbT1h5UuhF1ezVv3GYSCSHuu3vHzoO4lrrIOmOdcPlSw+BuSy3WOpS9OIud4IwZ +UFbzJNs6cOZbnwz7lwBzXFkm3PXPlwXmlGUPF9S4My+F4hYONE4zxI4IcmeF6U43 +JUX0xbD+LXNnezCLkvkcVjMCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/src/Dependency/DependencyDumper.php b/src/Dependency/DependencyDumper.php deleted file mode 100644 index 61e6bc5c32..0000000000 --- a/src/Dependency/DependencyDumper.php +++ /dev/null @@ -1,137 +0,0 @@ -dependencyResolver = $dependencyResolver; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->fileHelper = $fileHelper; - $this->parser = $parser; - $this->scopeFactory = $scopeFactory; - $this->fileFinder = $fileFinder; - } - - /** - * @param string[] $files - * @param callable(int $count): void $countCallback - * @param callable(): void $progressCallback - * @param string[]|null $analysedPaths - * @return string[][] - */ - public function dumpDependencies( - array $files, - callable $countCallback, - callable $progressCallback, - ?array $analysedPaths - ): array - { - $analysedFiles = $files; - if ($analysedPaths !== null) { - $analysedFiles = $this->fileFinder->findFiles($analysedPaths)->getFiles(); - } - $this->nodeScopeResolver->setAnalysedFiles($analysedFiles); - $analysedFiles = array_fill_keys($analysedFiles, true); - - $dependencies = []; - $countCallback(count($files)); - foreach ($files as $file) { - try { - $parserNodes = $this->parser->parseFile($file); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - continue; - } - - $fileDependencies = []; - try { - $this->nodeScopeResolver->processNodes( - $parserNodes, - $this->scopeFactory->create(ScopeContext::create($file)), - function (\PhpParser\Node $node, Scope $scope) use ($analysedFiles, &$fileDependencies): void { - $fileDependencies = array_merge( - $fileDependencies, - $this->resolveDependencies($node, $scope, $analysedFiles) - ); - } - ); - } catch (\PHPStan\AnalysedCodeException $e) { - // pass - } - - foreach (array_unique($fileDependencies) as $fileDependency) { - $relativeDependencyFile = $fileDependency; - $dependencies[$relativeDependencyFile][] = $file; - } - - $progressCallback(); - } - - return $dependencies; - } - - /** - * @param \PhpParser\Node $node - * @param Scope $scope - * @param array $analysedFiles - * @return string[] - */ - private function resolveDependencies( - \PhpParser\Node $node, - Scope $scope, - array $analysedFiles - ): array - { - $dependencies = []; - - foreach ($this->dependencyResolver->resolveDependencies($node, $scope) as $dependencyReflection) { - $dependencyFile = $dependencyReflection->getFileName(); - if ($dependencyFile === false) { - continue; - } - $dependencyFile = $this->fileHelper->normalizePath($dependencyFile); - - if ($scope->getFile() === $dependencyFile) { - continue; - } - - if (!isset($analysedFiles[$dependencyFile])) { - continue; - } - - $dependencies[$dependencyFile] = $dependencyFile; - } - - return array_values($dependencies); - } - -} diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 0491b7f8ce..77a4f957fe 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -2,95 +2,160 @@ namespace PHPStan\Dependency; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\Function_; use PHPStan\Analyser\Scope; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; +use PHPStan\File\FileHelper; +use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Node\InFunctionNode; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Reflection\ReflectionWithFilename; use PHPStan\Type\ClosureType; +use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Type; +use function array_merge; +use function count; -class DependencyResolver +final class DependencyResolver { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct( + private FileHelper $fileHelper, + private ReflectionProvider $reflectionProvider, + private ExportedNodeResolver $exportedNodeResolver, + private FileTypeMapper $fileTypeMapper, + ) { - $this->reflectionProvider = $reflectionProvider; } - /** - * @param \PhpParser\Node $node - * @param Scope $scope - * @return ReflectionWithFilename[] - */ - public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array + public function resolveDependencies(Node $node, Scope $scope): NodeDependencies { $dependenciesReflections = []; - if ($node instanceof \PhpParser\Node\Stmt\Class_) { + 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); } foreach ($node->implements as $className) { $this->addClassToDependencies($className->toString(), $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Stmt\Interface_) { - if ($node->extends !== null) { - foreach ($node->extends as $className) { - $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()); - if ($parametersAcceptor instanceof \PHPStan\Reflection\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); } } - } elseif ($node instanceof Function_) { - $functionName = $node->name->name; - if (isset($node->namespacedName)) { - $functionName = (string) $node->namespacedName; + if ($nativeMethod->getSelfOutType() !== null) { + foreach ($nativeMethod->getSelfOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - $functionNameName = new Name($functionName); - if ($this->reflectionProvider->hasFunction($functionNameName, null)) { - $functionReflection = $this->reflectionProvider->getFunction($functionNameName, null); - - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - - if ($parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + } elseif ($node instanceof ClassPropertyNode) { + $nativeType = $node->getNativeType(); + if ($nativeType !== null) { + foreach ($nativeType->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) { + $phpDocType = $node->getPhpDocType(); + if ($phpDocType !== null) { + foreach ($phpDocType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + } elseif ($node instanceof InFunctionNode) { + $functionReflection = $node->getFunctionReflection(); + $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); - foreach ($returnTypeReferencedClasses as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + $this->extractFromParametersAcceptor($functionReflection, $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); + } } - } elseif ($node instanceof \PhpParser\Node\Expr\FuncCall) { + } 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); + } + } + } elseif ($node instanceof Node\Expr\FuncCall) { $functionName = $node->name; - if ($functionName instanceof \PhpParser\Node\Name) { + if ($functionName instanceof Node\Name) { try { - $dependenciesReflections[] = $this->getFunctionReflection($functionName, $scope); - } catch (\PHPStan\Broker\FunctionNotFoundException $e) { + $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 } } else { @@ -102,6 +167,23 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array 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); + } + } } } } @@ -110,8 +192,9 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Expr\MethodCall || $node instanceof \PhpParser\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); } @@ -120,12 +203,155 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ( - $node instanceof \PhpParser\Node\Expr\StaticCall - || $node instanceof \PhpParser\Node\Expr\ClassConstFetch - || $node instanceof \PhpParser\Node\Expr\StaticPropertyFetch - ) { - if ($node->class instanceof \PhpParser\Node\Name) { + + 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 { + 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)) { + $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) { @@ -137,20 +363,70 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array 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 \PhpParser\Node\Expr\New_ - && $node->class instanceof \PhpParser\Node\Name + $node instanceof Node\Expr\New_ + && $node->class instanceof Node\Name ) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); - } elseif ($node instanceof \PhpParser\Node\Stmt\TraitUse) { + } elseif ($node instanceof Node\Stmt\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); } - } elseif ($node instanceof \PhpParser\Node\Expr\Instanceof_) { + + $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); } - } elseif ($node instanceof \PhpParser\Node\Stmt\Catch_) { + } elseif ($node instanceof Node\Stmt\Catch_) { foreach ($node->types as $type) { $this->addClassToDependencies($scope->resolveName($type), $dependenciesReflections); } @@ -164,15 +440,19 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array } 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 ($node instanceof Array_) { + } elseif ( + $node instanceof Array_ + && $this->considerArrayForCallableTest($scope, $node) + ) { $arrayType = $scope->getType($node); if (!$arrayType->isCallable()->no()) { foreach ($arrayType->getCallableParametersAcceptors($scope) as $variant) { @@ -184,18 +464,28 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): array } } - return $dependenciesReflections; + return new NodeDependencies($this->fileHelper, $dependenciesReflections, $this->exportedNodeResolver->resolve($scope->getFile(), $node)); + } + + private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): bool + { + $items = $arrayNode->items; + if (count($items) !== 2) { + return false; + } + + $itemType = $scope->getType($items[0]->value); + return $itemType->isClassString()->yes(); } /** - * @param string $className - * @param ReflectionWithFilename[] $dependenciesReflections + * @param array $dependenciesReflections */ private function addClassToDependencies(string $className, array &$dependenciesReflections): void { try { $classReflection = $this->reflectionProvider->getClass($className); - } catch (\PHPStan\Broker\ClassNotFoundException $e) { + } catch (ClassNotFoundException) { return; } @@ -210,47 +500,181 @@ 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 !== false); + } while ($classReflection !== null); } - private function getFunctionReflection(\PhpParser\Node\Name $nameNode, ?Scope $scope): ReflectionWithFilename + private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): FunctionReflection { - $reflection = $this->reflectionProvider->getFunction($nameNode, $scope); - if (!$reflection instanceof ReflectionWithFilename) { - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); - } - - return $reflection; + return $this->reflectionProvider->getFunction($nameNode, $scope); } /** - * @param ParametersAcceptorWithPhpDocs $parametersAcceptor - * @param ReflectionWithFilename[] $dependenciesReflections + * @param array $dependenciesReflections */ private function extractFromParametersAcceptor( - ParametersAcceptorWithPhpDocs $parametersAcceptor, - array &$dependenciesReflections + ExtendedParametersAcceptor $parametersAcceptor, + array &$dependenciesReflections, ): void { foreach ($parametersAcceptor->getParameters() as $parameter) { $referencedClasses = array_merge( $parameter->getNativeType()->getReferencedClasses(), - $parameter->getPhpDocType()->getReferencedClasses() + $parameter->getPhpDocType()->getReferencedClasses(), ); 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( $parametersAcceptor->getNativeReturnType()->getReferencedClasses(), - $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses() + $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses(), ); foreach ($returnTypeReferencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + /** + * @param array $dependenciesReflections + */ + private function extractThrowType( + ?Type $throwType, + array &$dependenciesReflections, + ): void + { + if ($throwType === null) { + return; + } + + foreach ($throwType->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } diff --git a/src/Dependency/ExportedNode.php b/src/Dependency/ExportedNode.php new file mode 100644 index 0000000000..59c32f2a0c --- /dev/null +++ b/src/Dependency/ExportedNode.php @@ -0,0 +1,20 @@ + $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 new file mode 100644 index 0000000000..2aec4d44fa --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -0,0 +1,91 @@ +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 + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['value'], + $properties['attributes'], + ); + } + + /** + * @param mixed[] $data + */ + 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']), + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'value' => $this->value, + 'attributes' => $this->attributes, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php new file mode 100644 index 0000000000..e555874fc7 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php @@ -0,0 +1,106 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->constants) !== count($node->constants)) { + return false; + } + + foreach ($this->constants as $i => $constant) { + if (!$constant->equals($node->constants[$i])) { + return false; + } + } + + return $this->public === $node->public + && $this->private === $node->private + && $this->final === $node->final; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['constants'], + $properties['public'], + $properties['private'], + $properties['final'], + $properties['phpDoc'], + ); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + array_map(static function (array $constantData): ExportedClassConstantNode { + if ($constantData['type'] !== ExportedClassConstantNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedClassConstantNode::decode($constantData['data']); + }, $data['constants']), + $data['public'], + $data['private'], + $data['final'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'constants' => $this->constants, + 'public' => $this->public, + 'private' => $this->private, + 'final' => $this->final, + 'phpDoc' => $this->phpDoc, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php new file mode 100644 index 0000000000..cb57bddb4c --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -0,0 +1,185 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->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; + } + + foreach ($this->traitUseAdaptations as $i => $ourTraitUseAdaptation) { + $theirTraitUseAdaptation = $node->traitUseAdaptations[$i]; + if (!$ourTraitUseAdaptation->equals($theirTraitUseAdaptation)) { + return false; + } + } + + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + + return $this->name === $node->name + && $this->abstract === $node->abstract + && $this->final === $node->final + && $this->extends === $node->extends + && $this->implements === $node->implements + && $this->usedTraits === $node->usedTraits; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['phpDoc'], + $properties['abstract'], + $properties['final'], + $properties['extends'], + $properties['implements'], + $properties['usedTraits'], + $properties['traitUseAdaptations'], + $properties['statements'], + $properties['attributes'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'phpDoc' => $this->phpDoc, + 'abstract' => $this->abstract, + 'final' => $this->final, + 'extends' => $this->extends, + 'implements' => $this->implements, + 'usedTraits' => $this->usedTraits, + 'traitUseAdaptations' => $this->traitUseAdaptations, + 'statements' => $this->statements, + 'attributes' => $this->attributes, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['abstract'], + $data['final'], + $data['extends'], + $data['implements'], + $data['usedTraits'], + array_map(static function (array $traitUseAdaptationData): ExportedTraitUseAdaptation { + if ($traitUseAdaptationData['type'] !== ExportedTraitUseAdaptation::class) { + throw new ShouldNotHappenException(); + } + return ExportedTraitUseAdaptation::decode($traitUseAdaptationData['data']); + }, $data['traitUseAdaptations']), + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + 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 new file mode 100644 index 0000000000..65b3273315 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php @@ -0,0 +1,78 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + return $this->name === $node->name + && $this->value === $node->value; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['value'], + $properties['phpDoc'], + ); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['value'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'value' => $this->value, + 'phpDoc' => $this->phpDoc, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php new file mode 100644 index 0000000000..b703518ab9 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -0,0 +1,148 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + + 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; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['scalarType'], + $properties['phpDoc'], + $properties['implements'], + $properties['statements'], + $properties['attributes'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'scalarType' => $this->scalarType, + 'phpDoc' => $this->phpDoc, + 'implements' => $this->implements, + 'statements' => $this->statements, + 'attributes' => $this->attributes, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['scalarType'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['implements'], + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), + 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 new file mode 100644 index 0000000000..d0ebd50613 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -0,0 +1,147 @@ +parameters) !== count($node->parameters)) { + return false; + } + + foreach ($this->parameters as $i => $ourParameter) { + $theirParameter = $node->parameters[$i]; + if (!$ourParameter->equals($theirParameter)) { + return false; + } + } + + if ($this->phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->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; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['phpDoc'], + $properties['byRef'], + $properties['returnType'], + $properties['parameters'], + $properties['attributes'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'phpDoc' => $this->phpDoc, + 'byRef' => $this->byRef, + 'returnType' => $this->returnType, + 'parameters' => $this->parameters, + 'attributes' => $this->attributes, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['byRef'], + $data['returnType'], + array_map(static function (array $parameterData): ExportedParameterNode { + if ($parameterData['type'] !== ExportedParameterNode::class) { + throw new ShouldNotHappenException(); + } + 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 new file mode 100644 index 0000000000..0e6d6632db --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedInterfaceNode.php @@ -0,0 +1,117 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + + return $this->name === $node->name + && $this->extends === $node->extends; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['phpDoc'], + $properties['extends'], + $properties['statements'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'phpDoc' => $this->phpDoc, + 'extends' => $this->extends, + 'statements' => $this->statements, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['extends'], + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), + ); + } + + /** + * @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 new file mode 100644 index 0000000000..af2b4255ce --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -0,0 +1,158 @@ +parameters) !== count($node->parameters)) { + return false; + } + + foreach ($this->parameters as $i => $ourParameter) { + $theirParameter = $node->parameters[$i]; + if (!$ourParameter->equals($theirParameter)) { + return false; + } + } + + if ($this->phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->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 + && $this->private === $node->private + && $this->abstract === $node->abstract + && $this->final === $node->final + && $this->static === $node->static + && $this->returnType === $node->returnType; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['phpDoc'], + $properties['byRef'], + $properties['public'], + $properties['private'], + $properties['abstract'], + $properties['final'], + $properties['static'], + $properties['returnType'], + $properties['parameters'], + $properties['attributes'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'phpDoc' => $this->phpDoc, + 'byRef' => $this->byRef, + 'public' => $this->public, + 'private' => $this->private, + 'abstract' => $this->abstract, + 'final' => $this->final, + 'static' => $this->static, + 'returnType' => $this->returnType, + 'parameters' => $this->parameters, + 'attributes' => $this->attributes, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['byRef'], + $data['public'], + $data['private'], + $data['abstract'], + $data['final'], + $data['static'], + $data['returnType'], + array_map(static function (array $parameterData): ExportedParameterNode { + if ($parameterData['type'] !== ExportedParameterNode::class) { + throw new ShouldNotHappenException(); + } + 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 new file mode 100644 index 0000000000..9f4cf02d61 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -0,0 +1,106 @@ +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 + && $this->variadic === $node->variadic + && $this->hasDefault === $node->hasDefault; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['type'], + $properties['byRef'], + $properties['variadic'], + $properties['hasDefault'], + $properties['attributes'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'type' => $this->type, + 'byRef' => $this->byRef, + 'variadic' => $this->variadic, + 'hasDefault' => $this->hasDefault, + 'attributes' => $this->attributes, + ], + ]; + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['type'], + $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 new file mode 100644 index 0000000000..9288a58107 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedPhpDocNode.php @@ -0,0 +1,65 @@ + $uses alias(string) => fullName(string) + * @param array $constUses alias(string) => fullName(string) + */ + public function __construct(private string $phpDocString, private ?string $namespace, private array $uses, private array $constUses) + { + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + return $this->phpDocString === $node->phpDocString + && $this->namespace === $node->namespace + && $this->uses === $node->uses + && $this->constUses === $node->constUses; + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'phpDocString' => $this->phpDocString, + 'namespace' => $this->namespace, + 'uses' => $this->uses, + 'constUses' => $this->constUses, + ], + ]; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self($properties['phpDocString'], $properties['namespace'], $properties['uses'], $properties['constUses'] ?? []); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + 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 new file mode 100644 index 0000000000..af58d51738 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -0,0 +1,137 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->names) !== count($node->names)) { + return false; + } + + foreach ($this->names as $i => $name) { + if ($name !== $node->names[$i]) { + 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->type === $node->type + && $this->public === $node->public + && $this->private === $node->private + && $this->static === $node->static + && $this->readonly === $node->readonly; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['names'], + $properties['phpDoc'], + $properties['type'], + $properties['public'], + $properties['private'], + $properties['static'], + $properties['readonly'], + $properties['attributes'], + ); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['names'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['type'], + $data['public'], + $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']), + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'names' => $this->names, + 'phpDoc' => $this->phpDoc, + 'type' => $this->type, + 'public' => $this->public, + '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 new file mode 100644 index 0000000000..8f6808f2b3 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedTraitNode.php @@ -0,0 +1,65 @@ + self::class, + 'data' => [ + 'traitName' => $this->traitName, + ], + ]; + } + + /** + * @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 new file mode 100644 index 0000000000..85c515fac4 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php @@ -0,0 +1,106 @@ +traitName === $node->traitName + && $this->method === $node->method + && $this->newModifier === $node->newModifier + && $this->newName === $node->newName + && $this->insteadOfs === $node->insteadOfs; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['traitName'], + $properties['method'], + $properties['newModifier'], + $properties['newName'], + $properties['insteadOfs'], + ); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['traitName'], + $data['method'], + $data['newModifier'], + $data['newName'], + $data['insteadOfs'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'traitName' => $this->traitName, + 'method' => $this->method, + 'newModifier' => $this->newModifier, + 'newName' => $this->newName, + 'insteadOfs' => $this->insteadOfs, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNodeFetcher.php b/src/Dependency/ExportedNodeFetcher.php new file mode 100644 index 0000000000..dbc36c155a --- /dev/null +++ b/src/Dependency/ExportedNodeFetcher.php @@ -0,0 +1,38 @@ +addVisitor($this->visitor); + + try { + $ast = $this->parser->parseFile($fileName); + } catch (ParserErrorsException) { + return []; + } + $this->visitor->reset($fileName); + $nodeTraverser->traverse($ast); + + return $this->visitor->getExportedNodes(); + } + +} diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php new file mode 100644 index 0000000000..441c785c99 --- /dev/null +++ b/src/Dependency/ExportedNodeResolver.php @@ -0,0 +1,385 @@ +namespacedName)) { + $docComment = $node->getDocComment(); + $extendsName = null; + if ($node->extends !== null) { + $extendsName = $node->extends->toString(); + } + + $implementsNames = []; + foreach ($node->implements as $className) { + $implementsNames[] = $className->toString(); + } + + $usedTraits = []; + $adaptations = []; + foreach ($node->getTraitUses() as $traitUse) { + foreach ($traitUse->traits as $usedTraitName) { + $usedTraits[] = $usedTraitName->toString(); + } + foreach ($traitUse->adaptations as $adaptation) { + $adaptations[] = $adaptation; + } + } + + $className = $node->namespacedName->toString(); + + return new ExportedClassNode( + $className, + $this->exportPhpDocNode( + $fileName, + $className, + null, + $docComment !== null ? $docComment->getText() : null, + ), + $node->isAbstract(), + $node->isFinal(), + $extendsName, + $implementsNames, + $usedTraits, + array_map(static function (Node\Stmt\TraitUseAdaptation $adaptation): ExportedTraitUseAdaptation { + if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + return ExportedTraitUseAdaptation::createAlias( + $adaptation->trait !== null ? $adaptation->trait->toString() : null, + $adaptation->method->toString(), + $adaptation->newModifier, + $adaptation->newName !== null ? $adaptation->newName->toString() : null, + ); + } + + if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) { + return ExportedTraitUseAdaptation::createPrecedence( + $adaptation->trait !== null ? $adaptation->trait->toString() : null, + $adaptation->method->toString(), + array_map(static fn (Name $name): string => $name->toString(), $adaptation->insteadof), + ); + } + + throw new ShouldNotHappenException(); + }, $adaptations), + $this->exportClassStatements($node->stmts, $fileName, $className), + $this->exportAttributeNodes($node->attrGroups), + ); + } + + if ($node instanceof Node\Stmt\Interface_ && isset($node->namespacedName)) { + $extendsNames = array_map(static fn (Name $name): string => (string) $name, $node->extends); + $docComment = $node->getDocComment(); + + $interfaceName = $node->namespacedName->toString(); + + return new ExportedInterfaceNode( + $interfaceName, + $this->exportPhpDocNode( + $fileName, + $interfaceName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + $extendsNames, + $this->exportClassStatements($node->stmts, $fileName, $interfaceName), + ); + } + + if ($node instanceof Node\Stmt\Enum_ && $node->namespacedName !== null) { + $implementsNames = array_map(static fn (Name $name): string => (string) $name, $node->implements); + $docComment = $node->getDocComment(); + + $enumName = $node->namespacedName->toString(); + $scalarType = null; + if ($node->scalarType !== null) { + $scalarType = $node->scalarType->toString(); + } + + return new ExportedEnumNode( + $enumName, + $scalarType, + $this->exportPhpDocNode( + $fileName, + $enumName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + $implementsNames, + $this->exportClassStatements($node->stmts, $fileName, $enumName), + $this->exportAttributeNodes($node->attrGroups), + ); + } + + if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { + return new ExportedTraitNode($node->namespacedName->toString()); + } + + if ($node instanceof Function_) { + $functionName = $node->name->name; + if (isset($node->namespacedName)) { + $functionName = (string) $node->namespacedName; + } + + $docComment = $node->getDocComment(); + + return new ExportedFunctionNode( + $functionName, + $this->exportPhpDocNode( + $fileName, + null, + $functionName, + $docComment !== null ? $docComment->getText() : null, + ), + $node->byRef, + NodeTypePrinter::printType($node->returnType), + $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), + ); + } + + return null; + } + + /** + * @param Node\Param[] $params + * @return ExportedParameterNode[] + */ + private function exportParameterNodes(array $params): array + { + $nodes = []; + foreach ($params as $param) { + if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { + throw new ShouldNotHappenException(); + } + $type = $param->type; + if ( + $type !== null + && $param->default instanceof Node\Expr\ConstFetch + && $param->default->name->toLowerString() === 'null' + ) { + if ($type instanceof Node\UnionType) { + $innerTypes = $type->types; + $innerTypes[] = new Name('null'); + $type = new Node\UnionType($innerTypes); + } elseif ($type instanceof Node\Identifier || $type instanceof Name) { + $type = new Node\NullableType($type); + } + } + $nodes[] = new ExportedParameterNode( + $param->var->name, + NodeTypePrinter::printType($type), + $param->byRef, + $param->variadic, + $param->default !== null, + $this->exportAttributeNodes($param->attrGroups), + ); + } + + return $nodes; + } + + private function exportPhpDocNode( + string $file, + ?string $className, + ?string $functionName, + ?string $text, + ): ?ExportedPhpDocNode + { + if ($text === null) { + return null; + } + + $resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( + $file, + $className, + null, + $functionName, + $text, + ); + + $nameScope = $resolvedPhpDocBlock->getNullableNameScope(); + if ($nameScope === null) { + return null; + } + + return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses(), $nameScope->getConstUses()); + } + + /** + * @param Node\Stmt[] $statements + * @return ExportedNode[] + */ + private function exportClassStatements(array $statements, string $fileName, string $namespacedName): array + { + $exportedNodes = []; + foreach ($statements as $statement) { + $exportedNode = $this->exportClassStatement($statement, $fileName, $namespacedName); + if ($exportedNode === null) { + continue; + } + + $exportedNodes[] = $exportedNode; + } + + return $exportedNodes; + } + + private function exportClassStatement(Node\Stmt $node, string $fileName, string $namespacedName): ?ExportedNode + { + if ($node instanceof ClassMethod) { + if ($node->isAbstract() || $node->isFinal() || !$node->isPrivate()) { + $methodName = $node->name->toString(); + $docComment = $node->getDocComment(); + + return new ExportedMethodNode( + $methodName, + $this->exportPhpDocNode( + $fileName, + $namespacedName, + $methodName, + $docComment !== null ? $docComment->getText() : null, + ), + $node->byRef, + $node->isPublic(), + $node->isPrivate(), + $node->isAbstract(), + $node->isFinal(), + $node->isStatic(), + NodeTypePrinter::printType($node->returnType), + $this->exportParameterNodes($node->params), + $this->exportAttributeNodes($node->attrGroups), + ); + } + } + + if ($node instanceof Node\Stmt\Property) { + if ($node->isPrivate()) { + return null; + } + + $docComment = $node->getDocComment(); + + return new ExportedPropertiesNode( + array_map(static fn (Node\PropertyItem $prop): string => $prop->name->toString(), $node->props), + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + NodeTypePrinter::printType($node->type), + $node->isPublic(), + $node->isPrivate(), + $node->isStatic(), + $node->isReadonly(), + $this->exportAttributeNodes($node->attrGroups), + ); + } + + if ($node instanceof Node\Stmt\ClassConst) { + if ($node->isPrivate()) { + return null; + } + + $docComment = $node->getDocComment(); + + $constants = []; + foreach ($node->consts as $const) { + $constants[] = new ExportedClassConstantNode( + $const->name->toString(), + $this->exprPrinter->printExpr($const->value), + $this->exportAttributeNodes($node->attrGroups), + ); + } + + return new ExportedClassConstantsNode( + $constants, + $node->isPublic(), + $node->isPrivate(), + $node->isFinal(), + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + ); + } + + if ($node instanceof Node\Stmt\EnumCase) { + $docComment = $node->getDocComment(); + + return new ExportedEnumCaseNode( + $node->name->toString(), + $node->expr !== null ? $this->exprPrinter->printExpr($node->expr) : null, + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + ); + } + + 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 new file mode 100644 index 0000000000..34dfd1efe1 --- /dev/null +++ b/src/Dependency/ExportedNodeVisitor.php @@ -0,0 +1,61 @@ +fileName = $fileName; + $this->currentNodes = []; + } + + /** + * @return RootExportedNode[] + */ + public function getExportedNodes(): array + { + return $this->currentNodes; + } + + public function enterNode(Node $node): ?int + { + if ($this->fileName === null) { + throw new ShouldNotHappenException(); + } + $exportedNode = $this->exportedNodeResolver->resolve($this->fileName, $node); + if ($exportedNode !== null) { + $this->currentNodes[] = $exportedNode; + } + + if ( + $node instanceof Node\Stmt\ClassMethod + || $node instanceof Node\Stmt\Function_ + || $node instanceof Node\Stmt\Trait_ + ) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + return null; + } + +} diff --git a/src/Dependency/NodeDependencies.php b/src/Dependency/NodeDependencies.php new file mode 100644 index 0000000000..ac30175b35 --- /dev/null +++ b/src/Dependency/NodeDependencies.php @@ -0,0 +1,58 @@ + $reflections + */ + public function __construct( + private FileHelper $fileHelper, + private array $reflections, + private ?RootExportedNode $exportedNode, + ) + { + } + + /** + * @param array $analysedFiles + * @return string[] + */ + public function getFileDependencies(string $currentFile, array $analysedFiles): array + { + $dependencies = []; + + foreach ($this->reflections as $dependencyReflection) { + $dependencyFile = $dependencyReflection->getFileName(); + if ($dependencyFile === null) { + continue; + } + $dependencyFile = $this->fileHelper->normalizePath($dependencyFile); + + if ($currentFile === $dependencyFile) { + continue; + } + + if (!isset($analysedFiles[$dependencyFile])) { + continue; + } + + $dependencies[$dependencyFile] = $dependencyFile; + } + + return array_values($dependencies); + } + + 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)); } @@ -39,10 +95,13 @@ public function beforeCompile(): void foreach ($config as $type => $tags) { $services = $builder->findByType($type); if (count($services) === 0) { - throw new \PHPStan\ShouldNotHappenException(sprintf('No services of type "%s" found.', $type)); + throw new ShouldNotHappenException(sprintf('No services of type "%s" found.', $type)); } foreach ($services as $service) { foreach ($tags as $tag => $parameter) { + 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 67f40bce3b..9536925b9d 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -2,18 +2,43 @@ 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\Configurator +final class Configurator extends \Nette\Bootstrap\Configurator { - private LoaderFactory $loaderFactory; + /** @var string[] */ + private array $allConfigFiles = []; - public function __construct(LoaderFactory $loaderFactory) + public function __construct(private LoaderFactory $loaderFactory, private bool $journalContainer) { - $this->loaderFactory = $loaderFactory; - parent::__construct(); } @@ -22,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[] */ @@ -30,17 +63,157 @@ protected function getDefaultParameters(): array return []; } + public function getContainerCacheDirectory(): string + { + return $this->getCacheDirectory() . '/nette.configurator'; + } + public function loadContainer(): string { $loader = new ContainerLoader( - $this->getCacheDirectory() . '/nette.configurator', - $this->parameters['debugMode'] + $this->getContainerCacheDirectory(), + $this->staticParameters['debugMode'], ); - return $loader->load( + $className = $loader->load( [$this, 'generateContainer'], - [$this->parameters, array_keys($this->dynamicParameters), $this->configs, PHP_VERSION_ID - PHP_RELEASE_VERSION, NeonAdapter::CACHE_KEY] + [$this->staticParameters, array_keys($this->dynamicParameters), $this->configs, PHP_VERSION_ID - PHP_RELEASE_VERSION, NeonAdapter::CACHE_KEY, $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 11410b5184..cd785677b1 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -2,31 +2,31 @@ namespace PHPStan\DependencyInjection; +/** @api */ interface Container { public function hasService(string $serviceName): bool; /** - * @param string $serviceName * @return mixed */ public function getService(string $serviceName); /** - * @param string $className - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className); /** - * @param string $className + * @param class-string $className * @return string[] */ public function findServiceNamesByType(string $className): array; /** - * @param string $tagName * @return mixed[] */ public function getServicesByTag(string $tagName): array; @@ -39,9 +39,8 @@ public function getParameters(): array; public function hasParameter(string $parameterName): bool; /** - * @param string $parameterName * @return mixed - * @throws \PHPStan\DependencyInjection\ParameterNotFoundException + * @throws ParameterNotFoundException */ public function getParameter(string $parameterName); diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 58fe7eeda2..c28e08a77c 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -2,26 +2,72 @@ namespace PHPStan\DependencyInjection; -use Nette\DI\Extensions\PhpExtension; +use Nette\Bootstrap\Extensions\PhpExtension; +use Nette\DI\Config\Adapters\PhpAdapter; +use Nette\DI\Definitions\Statement; +use Nette\DI\Extensions\ExtensionsExtension; +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 PHPStan\Broker\Broker; +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\Command\CommandHelper; use PHPStan\File\FileHelper; +use PHPStan\Node\Printer\Printer; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\PhpVersionStaticAccessor; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\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_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; -class ContainerFactory +/** + * @api + */ +final class ContainerFactory { - private string $currentWorkingDirectory; - private FileHelper $fileHelper; private string $rootDirectory; private string $configDirectory; - public function __construct(string $currentWorkingDirectory) + private static ?int $lastInitializedContainerId = null; + + private bool $journalContainer = false; + + /** @api */ + public function __construct(private string $currentWorkingDirectory) { - $this->currentWorkingDirectory = $currentWorkingDirectory; $this->fileHelper = new FileHelper($currentWorkingDirectory); $rootDir = __DIR__ . '/../..'; @@ -36,17 +82,16 @@ public function __construct(string $currentWorkingDirectory) $this->configDirectory = $originalRootDir . '/conf'; } + public function setJournalContainer(): void + { + $this->journalContainer = true; + } + /** - * @param string $tempDirectory * @param string[] $additionalConfigFiles * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $analysedPathsFromConfig - * @param string[] $allCustomConfigFiles - * @param string $usedLevel - * @param string|null $generateBaselineFile - * @param string|null $cliAutoloadFile - * @return \PHPStan\DependencyInjection\Container */ public function create( string $tempDirectory, @@ -54,21 +99,29 @@ public function create( array $analysedPaths, array $composerAutoloaderProjectPaths = [], array $analysedPathsFromConfig = [], - array $allCustomConfigFiles = [], string $usedLevel = CommandHelper::DEFAULT_LEVEL, ?string $generateBaselineFile = null, - ?string $cliAutoloadFile = null + ?string $cliAutoloadFile = 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 - )); + $generateBaselineFile, + ), $this->journalContainer); $configurator->defaultExtensions = [ 'php' => PhpExtension::class, - 'extensions' => \Nette\DI\Extensions\ExtensionsExtension::class, + 'extensions' => ExtensionsExtension::class, ]; $configurator->setDebugMode(true); $configurator->setTempDirectory($tempDirectory); @@ -78,26 +131,65 @@ public function create( 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1', 'tmpDir' => $tempDirectory, 'additionalConfigFiles' => $additionalConfigFiles, - 'analysedPaths' => $analysedPaths, + 'allConfigFiles' => $allConfigFiles, 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths, - 'analysedPathsFromConfig' => $analysedPathsFromConfig, - 'allCustomConfigFiles' => $allCustomConfigFiles, + 'generateBaselineFile' => $generateBaselineFile, 'usedLevel' => $usedLevel, 'cliAutoloadFile' => $cliAutoloadFile, + 'env' => getenv(), + ]); + $configurator->addDynamicParameters([ + 'analysedPaths' => $analysedPaths, + 'analysedPathsFromConfig' => $analysedPathsFromConfig, ]); $configurator->addConfig($this->configDirectory . '/config.neon'); foreach ($additionalConfigFiles as $additionalConfigFile) { $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'); + + /** @var Reflector $reflector */ + $reflector = $container->getService('betterReflectionReflector'); + + /** @var Parser $phpParser */ + $phpParser = $container->getService('phpParserDecorator'); + + BetterReflection::populate( + $container->getByType(PhpVersion::class)->getVersionId(), + $sourceLocator, + $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'); - return $container->getByType(Container::class); + BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); } public function getCurrentWorkingDirectory(): string @@ -115,4 +207,188 @@ 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); + } + + $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles); + + $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 [[], []]; + } + + 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]; + } + + private static function expandIncludedFile(string $includedFile, string $mainFile): string + { + return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute + ? $includedFile + : dirname($mainFile) . '/' . $includedFile; + } + + /** + * @param array $parameters + * @param array $parametersSchema + */ + private function validateParameters(array $parameters, array $parametersSchema): void + { + if (!(bool) $parameters['__validate']) { + return; + } + + $schema = $this->processArgument( + new Statement('schema', [ + new Statement('structure', [$parametersSchema]), + ]), + ); + $processor = new Processor(); + $processor->onNewContext[] = static function (SchemaContext $context): void { + $context->path = ['parameters']; + }; + $processor->process($schema, $parameters); + + 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.'); + } + } + + /** + * @param Statement[] $statements + */ + private function processSchema(array $statements, bool $required = true): Schema + { + if (count($statements) === 0) { + throw new ShouldNotHappenException(); + } + + $parameterSchema = null; + foreach ($statements as $statement) { + $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); + if ($parameterSchema === null) { + /** @var Type|AnyOf|Structure $parameterSchema */ + $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); + } else { + $parameterSchema->{$statement->getEntity()}(...$processedArguments); + } + } + + if ($required) { + $parameterSchema->required(); + } + + return $parameterSchema; + } + + /** + * @param mixed $argument + * @return mixed + */ + private function processArgument($argument, bool $required = true) + { + if ($argument instanceof Statement) { + if ($argument->entity === 'schema') { + $arguments = []; + foreach ($argument->arguments as $schemaArgument) { + if (!$schemaArgument instanceof Statement) { + throw new ShouldNotHappenException('schema() should contain another statement().'); + } + + $arguments[] = $schemaArgument; + } + + if (count($arguments) === 0) { + throw new ShouldNotHappenException('schema() should have at least one argument.'); + } + + return $this->processSchema($arguments, $required); + } + + return $this->processSchema([$argument], $required); + } elseif (is_array($argument)) { + $processedArray = []; + foreach ($argument as $key => $val) { + $required = $key[0] !== '?'; + $key = $required ? $key : substr($key, 1); + $processedArray[$key] = $this->processArgument($val, $required); + } + + return $processedArray; + } + + return $argument; + } + } diff --git a/src/DependencyInjection/DerivativeContainerFactory.php b/src/DependencyInjection/DerivativeContainerFactory.php index 6993c7b207..218eb4e252 100644 --- a/src/DependencyInjection/DerivativeContainerFactory.php +++ b/src/DependencyInjection/DerivativeContainerFactory.php @@ -2,70 +2,40 @@ namespace PHPStan\DependencyInjection; -class DerivativeContainerFactory -{ - - private string $currentWorkingDirectory; - - private string $tempDirectory; - - /** @var string[] */ - private array $additionalConfigFiles; - - /** @var string[] */ - private array $analysedPaths; +use function array_merge; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - - /** @var string[] */ - private array $analysedPathsFromConfig; - - /** @var string[] */ - private array $allCustomConfigFiles; - - private string $usedLevel; +final class DerivativeContainerFactory +{ /** - * @param string $currentWorkingDirectory - * @param string $tempDirectory * @param string[] $additionalConfigFiles * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $analysedPathsFromConfig - * @param string[] $allCustomConfigFiles - * @param string $usedLevel */ public function __construct( - string $currentWorkingDirectory, - string $tempDirectory, - array $additionalConfigFiles, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $analysedPathsFromConfig, - array $allCustomConfigFiles, - string $usedLevel + private string $currentWorkingDirectory, + private string $tempDirectory, + private array $additionalConfigFiles, + private array $analysedPaths, + private array $composerAutoloaderProjectPaths, + private array $analysedPathsFromConfig, + private string $usedLevel, + private ?string $generateBaselineFile, + private ?string $cliAutoloadFile, ) { - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->tempDirectory = $tempDirectory; - $this->additionalConfigFiles = $additionalConfigFiles; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->analysedPathsFromConfig = $analysedPathsFromConfig; - $this->allCustomConfigFiles = $allCustomConfigFiles; - $this->usedLevel = $usedLevel; } /** * @param string[] $additionalConfigFiles - * @return \PHPStan\DependencyInjection\Container */ public function create(array $additionalConfigFiles): Container { $containerFactory = new ContainerFactory( - $this->currentWorkingDirectory + $this->currentWorkingDirectory, ); + $containerFactory->setJournalContainer(); return $containerFactory->create( $this->tempDirectory, @@ -73,8 +43,9 @@ public function create(array $additionalConfigFiles): Container $this->analysedPaths, $this->composerAutoloaderProjectPaths, $this->analysedPathsFromConfig, - $this->allCustomConfigFiles, - $this->usedLevel + $this->usedLevel, + $this->generateBaselineFile, + $this->cliAutoloadFile, ); } 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 new file mode 100644 index 0000000000..bd4f2466a1 --- /dev/null +++ b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php @@ -0,0 +1,27 @@ +errors)); + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } + +} diff --git a/src/DependencyInjection/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 @@ +fileHelper = $fileHelper; - $this->rootDir = $rootDir; - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->generateBaselineFile = $generateBaselineFile; } public function createLoader(): Loader @@ -37,6 +26,7 @@ public function createLoader(): Loader $loader->setParameters([ 'rootDir' => $this->rootDir, 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'env' => getenv(), ]); return $loader; diff --git a/src/DependencyInjection/MemoizingContainer.php b/src/DependencyInjection/MemoizingContainer.php index d43641705c..bdd24bf291 100644 --- a/src/DependencyInjection/MemoizingContainer.php +++ b/src/DependencyInjection/MemoizingContainer.php @@ -2,17 +2,16 @@ namespace PHPStan\DependencyInjection; -class MemoizingContainer implements Container -{ +use function array_key_exists; - private Container $originalContainer; +final class MemoizingContainer implements Container +{ /** @var array */ private array $servicesByType = []; - public function __construct(Container $originalContainer) + public function __construct(private Container $originalContainer) { - $this->originalContainer = $originalContainer; } public function hasService(string $serviceName): bool @@ -20,19 +19,11 @@ public function hasService(string $serviceName): bool return $this->originalContainer->hasService($serviceName); } - /** - * @param string $serviceName - * @return mixed - */ public function getService(string $serviceName) { return $this->originalContainer->getService($serviceName); } - /** - * @param string $className - * @return mixed - */ public function getByType(string $className) { if (array_key_exists($className, $this->servicesByType)) { @@ -65,11 +56,6 @@ public function hasParameter(string $parameterName): bool return $this->originalContainer->hasParameter($parameterName); } - /** - * @param string $parameterName - * @return mixed - * @throws ParameterNotFoundException - */ public function getParameter(string $parameterName) { return $this->originalContainer->getParameter($parameterName); diff --git a/src/DependencyInjection/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((array) Neon::decode($contents), '', $file); - } catch (\Nette\Neon\Exception $e) { - throw new \Nette\Neon\Exception(sprintf('Error while loading %s: %s', $file, $e->getMessage())); + } catch (Exception $e) { + throw new Exception(sprintf('Error while loading %s: %s', $file, $e->getMessage())); } } @@ -45,12 +61,19 @@ public function process(array $arr, string $fileKey, string $file): array foreach ($arr as $key => $val) { if (is_string($key) && substr($key, -1) === self::PREVENT_MERGING_SUFFIX) { if (!is_array($val) && $val !== null) { - throw new \Nette\DI\InvalidConfigurationException(sprintf('Replacing operator is available only for arrays, item \'%s\' is not array.', $key)); + throw new InvalidConfigurationException(sprintf('Replacing operator is available only for arrays, item \'%s\' is not array.', $key)); } $key = substr($key, 0, -1); $val[Helpers::PREVENT_MERGING] = true; } + $keyToResolve = $fileKey; + if (is_int($key)) { + $keyToResolve .= '[]'; + } else { + $keyToResolve .= '[' . $key . ']'; + } + if (is_array($val)) { if (!is_int($key)) { $fileKeyToPass = $fileKey . '[' . $key . ']'; @@ -70,46 +93,63 @@ public function process(array $arr, string $fileKey, string $file): array foreach ($this->process($val->attributes, $fileKeyToPass, $file) as $st) { $tmp = new Statement( $tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')], - $st->arguments + $st->arguments, ); } $val = $tmp; } else { - $tmp = $this->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][autoload_files][]', - '[parameters][autoload_directories][]', '[parameters][paths][]', - '[parameters][excludes_analyse][]', + '[parameters][excludePaths][]', + '[parameters][excludePaths][analyse][]', + '[parameters][excludePaths][analyseAndScan][]', '[parameters][ignoreErrors][][paths][]', '[parameters][ignoreErrors][][path]', - '[parameters][bootstrap]', '[parameters][bootstrapFiles][]', '[parameters][scanFiles][]', '[parameters][scanDirectories][]', '[parameters][tmpDir]', + '[parameters][pro][tmpDir]', '[parameters][memoryLimitFile]', '[parameters][benchmarkFile]', '[parameters][stubFiles][]', - '[parameters][symfony][console_application_loader]', - '[parameters][symfony][container_xml_path]', + '[parameters][symfony][consoleApplicationLoader]', + '[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)); } + if ( + $keyToResolve === '[parameters][excludePaths]' + && $val !== null + && array_values($val) === $val + ) { + $val = ['analyseAndScan' => $val, 'analyse' => []]; + } + $res[$key] = $val; } return $res; @@ -117,7 +157,6 @@ public function process(array $arr, string $fileKey, string $file): array /** * @param mixed[] $data - * @return string */ public function dump(array $data): string { @@ -129,7 +168,7 @@ static function (&$val): void { } $val = self::statementToEntity($val); - } + }, ); return "# generated by Nette\n\n" . Neon::encode($data, Neon::BLOCK); } @@ -144,7 +183,7 @@ static function (&$val): void { } elseif ($val instanceof Reference) { $val = '@' . $val->getValue(); } - } + }, ); $entity = $val->getEntity(); @@ -157,7 +196,7 @@ static function (&$val): void { [ self::statementToEntity($entity[0]), new Entity('::' . $entity[1], $val->arguments), - ] + ], ); } elseif ($entity[0] instanceof Reference) { $entity = '@' . $entity[0]->getValue() . '::' . $entity[1]; diff --git a/src/DependencyInjection/NeonLoader.php b/src/DependencyInjection/NeonLoader.php index cee5eb5e1e..010b80cfab 100644 --- a/src/DependencyInjection/NeonLoader.php +++ b/src/DependencyInjection/NeonLoader.php @@ -2,27 +2,20 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\Config\Loader; use PHPStan\File\FileHelper; -class NeonLoader extends \Nette\DI\Config\Loader +final class NeonLoader extends Loader { - private FileHelper $fileHelper; - - private ?string $generateBaselineFile; - public function __construct( - FileHelper $fileHelper, - ?string $generateBaselineFile + private FileHelper $fileHelper, + private ?string $generateBaselineFile, ) { - $this->fileHelper = $fileHelper; - $this->generateBaselineFile = $generateBaselineFile; } /** - * @param string $file - * @param bool|null $merge * @return mixed[] */ public function load(string $file, ?bool $merge = true): array diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 5e57025649..914d0a43f1 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -3,18 +3,19 @@ namespace PHPStan\DependencyInjection\Nette; use PHPStan\DependencyInjection\Container; +use PHPStan\DependencyInjection\ParameterNotFoundException; +use function array_key_exists; +use function array_keys; +use function array_map; /** * @internal */ -class NetteContainer implements Container +final class NetteContainer implements Container { - private \Nette\DI\Container $container; - - public function __construct(\Nette\DI\Container $container) + public function __construct(private \Nette\DI\Container $container) { - $this->container = $container; } public function hasService(string $serviceName): bool @@ -23,7 +24,6 @@ public function hasService(string $serviceName): bool } /** - * @param string $serviceName * @return mixed */ public function getService(string $serviceName) @@ -32,8 +32,9 @@ public function getService(string $serviceName) } /** - * @param string $className - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className) { @@ -41,7 +42,7 @@ public function getByType(string $className) } /** - * @param string $className + * @param class-string $className * @return string[] */ public function findServiceNamesByType(string $className): array @@ -50,7 +51,6 @@ public function findServiceNamesByType(string $className): array } /** - * @param string $tagName * @return mixed[] */ public function getServicesByTag(string $tagName): array @@ -63,25 +63,24 @@ 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()); } /** - * @param string $parameterName * @return mixed */ public function getParameter(string $parameterName) { if (!$this->hasParameter($parameterName)) { - throw new \PHPStan\DependencyInjection\ParameterNotFoundException($parameterName); + throw new ParameterNotFoundException($parameterName); } - return $this->container->parameters[$parameterName]; + return $this->container->getParameter($parameterName); } /** @@ -90,9 +89,7 @@ public function getParameter(string $parameterName) */ private function tagsToServices(array $tags): array { - return array_map(function (string $serviceName) { - return $this->getService($serviceName); - }, array_keys($tags)); + return array_map(fn (string $serviceName) => $this->getService($serviceName), array_keys($tags)); } } diff --git a/src/DependencyInjection/ParameterNotFoundException.php b/src/DependencyInjection/ParameterNotFoundException.php index 393c5cd779..ad461ddef5 100644 --- a/src/DependencyInjection/ParameterNotFoundException.php +++ b/src/DependencyInjection/ParameterNotFoundException.php @@ -2,7 +2,10 @@ namespace PHPStan\DependencyInjection; -class ParameterNotFoundException extends \Exception +use Exception; +use function sprintf; + +final class ParameterNotFoundException extends Exception { public function __construct(string $parameterName) diff --git a/src/DependencyInjection/ParametersSchemaExtension.php b/src/DependencyInjection/ParametersSchemaExtension.php index ae35d20efa..9a703ed8e9 100644 --- a/src/DependencyInjection/ParametersSchemaExtension.php +++ b/src/DependencyInjection/ParametersSchemaExtension.php @@ -2,94 +2,17 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\CompilerExtension; use Nette\DI\Definitions\Statement; use Nette\Schema\Expect; use Nette\Schema\Schema; -class ParametersSchemaExtension extends \Nette\DI\CompilerExtension +final class ParametersSchemaExtension extends CompilerExtension { - public function getConfigSchema(): \Nette\Schema\Schema + public function getConfigSchema(): Schema { return Expect::arrayOf(Expect::type(Statement::class))->min(1); } - public function loadConfiguration(): void - { - /** @var mixed[] $config */ - $config = $this->config; - $config['__parametersSchema'] = new Statement(Schema::class); - $builder = $this->getContainerBuilder(); - $builder->parameters['__parametersSchema'] = $this->processArgument( - new Statement('schema', [ - new Statement('structure', [$config]), - ]) - ); - } - - /** - * @param Statement[] $statements - * @return \Nette\Schema\Schema - */ - private function processSchema(array $statements): Schema - { - if (count($statements) === 0) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $parameterSchema = null; - foreach ($statements as $statement) { - $processedArguments = array_map(function ($argument) { - return $this->processArgument($argument); - }, $statement->arguments); - if ($parameterSchema === null) { - /** @var \Nette\Schema\Elements\Type|\Nette\Schema\Elements\AnyOf|\Nette\Schema\Elements\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 \PHPStan\ShouldNotHappenException('schema() should contain another statement().'); - } - - $arguments[] = $schemaArgument; - } - - if (count($arguments) === 0) { - throw new \PHPStan\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 06133de037..0000000000 --- a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php +++ /dev/null @@ -1,61 +0,0 @@ -propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; - } - - public function setBroker(Broker $broker): void - { - $this->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 41c35dacdf..7e47b6498c 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -2,23 +2,25 @@ 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 \PHPStan\DependencyInjection\Container $container; + private ?ClassReflectionExtensionRegistry $registry = null; - private ?\PHPStan\Reflection\ClassReflectionExtensionRegistry $registry = null; - - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): ClassReflectionExtensionRegistry @@ -28,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 b72f286754..f38caac26d 100644 --- a/src/DependencyInjection/RulesExtension.php +++ b/src/DependencyInjection/RulesExtension.php @@ -2,13 +2,15 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\CompilerExtension; use Nette\Schema\Expect; -use PHPStan\Rules\RegistryFactory; +use Nette\Schema\Schema; +use PHPStan\Rules\LazyRegistry; -class RulesExtension extends \Nette\DI\CompilerExtension +final class RulesExtension extends CompilerExtension { - public function getConfigSchema(): \Nette\Schema\Schema + public function getConfigSchema(): Schema { return Expect::listOf('string'); } @@ -22,8 +24,8 @@ public function loadConfiguration(): void foreach ($config as $key => $rule) { $builder->addDefinition($this->prefix((string) $key)) ->setFactory($rule) - ->setAutowired(false) - ->addTag(RegistryFactory::RULE_TAG); + ->setAutowired($rule) + ->addTag(LazyRegistry::RULE_TAG); } } diff --git a/src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php b/src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php deleted file mode 100644 index 3759ec24ed..0000000000 --- a/src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php +++ /dev/null @@ -1,83 +0,0 @@ -dynamicMethodReturnTypeExtensions = $dynamicMethodReturnTypeExtensions; - $this->dynamicStaticMethodReturnTypeExtensions = $dynamicStaticMethodReturnTypeExtensions; - $this->dynamicFunctionReturnTypeExtensions = $dynamicFunctionReturnTypeExtensions; - } - - public function setBroker(Broker $broker): void - { - $this->broker = $broker; - } - - public function setReflectionProvider(ReflectionProvider $reflectionProvider): void - { - $this->reflectionProvider = $reflectionProvider; - } - - public function addDynamicMethodReturnTypeExtension(DynamicMethodReturnTypeExtension $extension): void - { - $this->dynamicMethodReturnTypeExtensions[] = $extension; - } - - public function addDynamicStaticMethodReturnTypeExtension(DynamicStaticMethodReturnTypeExtension $extension): void - { - $this->dynamicStaticMethodReturnTypeExtensions[] = $extension; - } - - public function addDynamicFunctionReturnTypeExtension(DynamicFunctionReturnTypeExtension $extension): void - { - $this->dynamicFunctionReturnTypeExtensions[] = $extension; - } - - public function getRegistry(): DynamicReturnTypeExtensionRegistry - { - return new DynamicReturnTypeExtensionRegistry( - $this->broker, - $this->reflectionProvider, - $this->dynamicMethodReturnTypeExtensions, - $this->dynamicStaticMethodReturnTypeExtensions, - $this->dynamicFunctionReturnTypeExtensions - ); - } - -} diff --git a/src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php deleted file mode 100644 index 80f73d7b5a..0000000000 --- a/src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php +++ /dev/null @@ -1,38 +0,0 @@ -extensions = $extensions; - } - - public function setBroker(Broker $broker): void - { - $this->broker = $broker; - } - - public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry - { - return new OperatorTypeSpecifyingExtensionRegistry( - $this->broker, - $this->extensions - ); - } - -} diff --git a/src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php b/src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php new file mode 100644 index 0000000000..9f67221cf1 --- /dev/null +++ b/src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php @@ -0,0 +1,21 @@ +container = $container; } public function getRegistry(): DynamicReturnTypeExtensionRegistry { if ($this->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), - $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG) + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), ); } diff --git a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php new file mode 100644 index 0000000000..0eb55cbf5b --- /dev/null +++ b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php @@ -0,0 +1,33 @@ +container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getDynamicMethodThrowTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getDynamicStaticMethodThrowTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} 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 3f9f0ca376..2be97ee777 100644 --- a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -2,28 +2,24 @@ 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 \PHPStan\DependencyInjection\Container $container; + private ?OperatorTypeSpecifyingExtensionRegistry $registry = null; - private ?\PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry $registry = null; - - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry { if ($this->registry === null) { $this->registry = new OperatorTypeSpecifyingExtensionRegistry( - $this->container->getByType(Broker::class), - $this->container->getServicesByTag(BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG) + $this->container->getServicesByTag(BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG), ); } diff --git a/src/DependencyInjection/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 new file mode 100644 index 0000000000..68a41d3e9f --- /dev/null +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -0,0 +1,226 @@ +getContainerBuilder(); + if (!$builder->parameters['__validate']) { + return; + } + + $ignoreErrors = $builder->parameters['ignoreErrors']; + if (count($ignoreErrors) === 0) { + return; + } + + /** @throws void */ + $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($phpDocParserConfig), + new TypeParser($phpDocParserConfig, new ConstExprParser($phpDocParserConfig)), + new TypeNodeResolver( + new DirectTypeNodeResolverExtensionRegistryProvider( + new class implements TypeNodeResolverExtensionRegistry { + + public function getExtensions(): array + { + return []; + } + + }, + ), + $reflectionProviderProvider, + new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver { + + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool + { + return false; + } + + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + return null; + } + + }), + $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 = []; + foreach ($ignoreErrors as $ignoreError) { + 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 { + continue; + } + } else { + $ignoreMessages = [$ignoreError]; + } + + foreach ($ignoreMessages as $ignoreMessage) { + $error = $this->validateMessage($ignoredRegexValidator, $ignoreMessage); + if ($error === null) { + continue; + } + $errors[] = $error; + } + } + + $reportUnmatched = (bool) $builder->parameters['reportUnmatchedIgnoredErrors']; + + if ($reportUnmatched) { + foreach ($ignoreErrors as $ignoreError) { + if (!is_array($ignoreError)) { + continue; + } + + if (isset($ignoreError['path'])) { + $ignorePaths = [$ignoreError['path']]; + } elseif (isset($ignoreError['paths'])) { + $ignorePaths = $ignoreError['paths']; + } else { + continue; + } + + 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); + } + } + } + + if (count($errors) === 0) { + return; + } + + 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 + */ + private function createIgnoredTypesError(string $regex, array $ignoredTypes): string + { + return sprintf( + "Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s", + $regex, + sprintf( + "It ignores all errors containing the following types:\n%s", + implode("\n", array_map(static fn (string $typeDescription): string => sprintf('* %s', $typeDescription), array_keys($ignoredTypes))), + ), + ); + } + + private function createAnchorInTheMiddleError(string $regex): string + { + return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex); + } + +} diff --git a/src/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 356d461a11..63d5a2e41d 100644 --- a/src/File/CouldNotReadFileException.php +++ b/src/File/CouldNotReadFileException.php @@ -2,7 +2,10 @@ namespace PHPStan\File; -class CouldNotReadFileException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use function sprintf; + +final class CouldNotReadFileException extends AnalysedCodeException { public function __construct(string $fileName) diff --git a/src/File/CouldNotWriteFileException.php b/src/File/CouldNotWriteFileException.php index 624f10e0d1..72e00464b7 100644 --- a/src/File/CouldNotWriteFileException.php +++ b/src/File/CouldNotWriteFileException.php @@ -2,7 +2,10 @@ namespace PHPStan\File; -class CouldNotWriteFileException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use function sprintf; + +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 4b60c7d5c4..ba314cbb67 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -2,28 +2,59 @@ namespace PHPStan\File; -class FileExcluder +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 substr; +use const DIRECTORY_SEPARATOR; +use const FNM_CASEFOLD; +use const FNM_NOESCAPE; + +final class FileExcluder { + /** + * Paths to exclude from analysing + * + * @var string[] + */ + private array $literalAnalyseExcludes = []; + /** * Directories to exclude from analysing * * @var string[] */ - private array $analyseExcludes; + 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[] + */ + private array $fnmatchAnalyseExcludes = []; + + private int $fnmatchFlags; /** - * @param FileHelper $fileHelper * @param string[] $analyseExcludes - * @param string[] $stubFiles */ public function __construct( - FileHelper $fileHelper, + private FileHelper $fileHelper, array $analyseExcludes, - array $stubFiles ) { - $this->analyseExcludes = array_map(function (string $exclude) use ($fileHelper): string { + foreach ($analyseExcludes as $exclude) { $len = strlen($exclude); $trailingDirSeparator = ($len > 0 && in_array($exclude[$len - 1], ['\\', '/'], true)); @@ -33,37 +64,71 @@ public function __construct( $normalized .= DIRECTORY_SEPARATOR; } - if ($this->isFnmatchPattern($normalized)) { - return $normalized; + if (self::isFnmatchPattern($normalized)) { + $this->fnmatchAnalyseExcludes[] = $normalized; + } else { + if (is_file($normalized)) { + $this->literalAnalyseFilesExcludes[] = $normalized; + } elseif (is_dir($normalized)) { + if (!$trailingDirSeparator) { + $normalized .= DIRECTORY_SEPARATOR; + } + + $this->literalAnalyseDirectoryExcludes[] = $normalized; + } } + } - return $fileHelper->absolutizePath($normalized); - }, array_merge($analyseExcludes, $stubFiles)); + $isWindows = DIRECTORY_SEPARATOR === '\\'; + if ($isWindows) { + $this->fnmatchFlags = FNM_NOESCAPE | FNM_CASEFOLD; + } else { + $this->fnmatchFlags = 0; + } } public function isExcludedFromAnalysing(string $file): bool { - foreach ($this->analyseExcludes as $exclude) { - if (strpos($file, $exclude) === 0) { + $file = $this->fileHelper->normalizePath($file); + + foreach ($this->literalAnalyseExcludes as $exclude) { + if (str_starts_with($file, $exclude)) { return true; } - - $isWindows = DIRECTORY_SEPARATOR === '\\'; - if ($isWindows) { - $fnmatchFlags = FNM_NOESCAPE | FNM_CASEFOLD; - } else { - $fnmatchFlags = 0; + } + foreach ($this->literalAnalyseDirectoryExcludes as $exclude) { + if (str_starts_with($file, $exclude)) { + return true; + } + } + foreach ($this->literalAnalyseFilesExcludes as $exclude) { + if ($file === $exclude) { + return true; } + } + foreach ($this->fnmatchAnalyseExcludes as $exclude) { + if (fnmatch($exclude, $file, $this->fnmatchFlags)) { + return true; + } + } - if ($this->isFnmatchPattern($exclude) && fnmatch($exclude, $file, $fnmatchFlags)) { + return false; + } + + 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; } - private function isFnmatchPattern(string $path): bool + public static function isFnmatchPattern(string $path): bool { return preg_match('~[*?[\]]~', $path) > 0; } diff --git a/src/File/FileExcluderFactory.php b/src/File/FileExcluderFactory.php new file mode 100644 index 0000000000..0bdae44c20 --- /dev/null +++ b/src/File/FileExcluderFactory.php @@ -0,0 +1,46 @@ +, analyseAndScan?: array} $excludePaths + */ + public function __construct( + private FileExcluderRawFactory $fileExcluderRawFactory, + private array $excludePaths, + ) + { + } + + public function createAnalyseFileExcluder(): FileExcluder + { + $paths = []; + if (array_key_exists('analyse', $this->excludePaths)) { + $paths = $this->excludePaths['analyse']; + } + if (array_key_exists('analyseAndScan', $this->excludePaths)) { + $paths = array_merge($paths, $this->excludePaths['analyseAndScan']); + } + + return $this->fileExcluderRawFactory->create(array_values(array_unique($paths))); + } + + public function createScanFileExcluder(): FileExcluder + { + $paths = []; + if (array_key_exists('analyseAndScan', $this->excludePaths)) { + $paths = $this->excludePaths['analyseAndScan']; + } + + return $this->fileExcluderRawFactory->create(array_values(array_unique($paths))); + } + +} diff --git a/src/File/FileExcluderRawFactory.php b/src/File/FileExcluderRawFactory.php new file mode 100644 index 0000000000..0e3550cb3a --- /dev/null +++ b/src/File/FileExcluderRawFactory.php @@ -0,0 +1,15 @@ +fileExcluder = $fileExcluder; - $this->fileHelper = $fileHelper; - $this->fileExtensions = $fileExtensions; } /** * @param string[] $paths - * @return FileFinderResult */ public function findFiles(array $paths): FileFinderResult { $onlyFiles = true; $files = []; foreach ($paths as $path) { - if (!file_exists($path)) { - throw new \PHPStan\File\PathNotFoundException($path); - } elseif (is_file($path)) { + if (is_file($path)) { $files[] = $this->fileHelper->normalizePath($path); + } elseif (!file_exists($path)) { + throw new PathNotFoundException($path); } else { $finder = new Finder(); $finder->followLinks(); @@ -53,9 +46,7 @@ public function findFiles(array $paths): FileFinderResult } } - $files = array_values(array_filter($files, function (string $file): bool { - return !$this->fileExcluder->isExcludedFromAnalysing($file); - })); + $files = array_values(array_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 db239b00cd..7ae7634c96 100644 --- a/src/File/FileFinderResult.php +++ b/src/File/FileFinderResult.php @@ -2,22 +2,14 @@ namespace PHPStan\File; -class FileFinderResult +final class FileFinderResult { - /** @var string[] */ - private array $files; - - private bool $onlyFiles; - /** * @param string[] $files - * @param bool $onlyFiles */ - public function __construct(array $files, bool $onlyFiles) + public function __construct(private array $files, private bool $onlyFiles) { - $this->files = $files; - $this->onlyFiles = $onlyFiles; } /** diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index b351f1e11f..32fad2d253 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -3,8 +3,22 @@ namespace PHPStan\File; use Nette\Utils\Strings; +use function array_pop; +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 strlen; +use function strtolower; +use function substr; +use function trim; +use const DIRECTORY_SEPARATOR; -class FileHelper +final class FileHelper { private string $workingDirectory; @@ -19,38 +33,52 @@ public function getWorkingDirectory(): string return $this->workingDirectory; } + /** @api */ 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 (\Nette\Utils\Strings::startsWith($path, 'phar://')) { + + if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) { return $path; } return rtrim($this->getWorkingDirectory(), '/\\') . DIRECTORY_SEPARATOR . ltrim($path, '/\\'); } + /** @api */ public function normalizePath(string $originalPath, string $directorySeparator = DIRECTORY_SEPARATOR): string { - $matches = \Nette\Utils\Strings::match($originalPath, '~^([a-z]+)\\:\\/\\/(.+)~'); + $isLocalPath = 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-z0-9+\-.]+)://(.+)$~is'); + } + if ($matches !== null) { [, $scheme, $path] = $matches; + $scheme = strtolower($scheme); } else { $scheme = null; $path = $originalPath; } - $path = str_replace('\\', '/', $path); - $path = Strings::replace($path, '~/{2,}~', '/'); + $path = str_replace(['\\', '//', '///', '////'], '/', $path); - $pathRoot = strpos($path, '/') === 0 ? $directorySeparator : ''; + $pathRoot = str_starts_with($path, '/') ? $directorySeparator : ''; $pathParts = explode('/', trim($path, '/')); $normalizedPathParts = []; @@ -59,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 new file mode 100644 index 0000000000..6fd0eaf8ef --- /dev/null +++ b/src/File/FileMonitor.php @@ -0,0 +1,93 @@ +|null */ + private ?array $fileHashes = null; + + /** @var array|null */ + private ?array $paths = null; + + public function __construct(private FileFinder $fileFinder) + { + } + + /** + * @param array $paths + */ + public function initialize(array $paths): void + { + $finderResult = $this->fileFinder->findFiles($paths); + $fileHashes = []; + foreach ($finderResult->getFiles() as $filePath) { + $fileHashes[$filePath] = $this->getFileHash($filePath); + } + + $this->fileHashes = $fileHashes; + $this->paths = $paths; + } + + public function getChanges(): FileMonitorResult + { + if ($this->fileHashes === null || $this->paths === null) { + throw new ShouldNotHappenException(); + } + $finderResult = $this->fileFinder->findFiles($this->paths); + $oldFileHashes = $this->fileHashes; + $fileHashes = []; + $newFiles = []; + $changedFiles = []; + $deletedFiles = []; + foreach ($finderResult->getFiles() as $filePath) { + if (!array_key_exists($filePath, $oldFileHashes)) { + $newFiles[] = $filePath; + $fileHashes[$filePath] = $this->getFileHash($filePath); + continue; + } + + $oldHash = $oldFileHashes[$filePath]; + unset($oldFileHashes[$filePath]); + $newHash = $this->getFileHash($filePath); + $fileHashes[$filePath] = $newHash; + if ($oldHash === $newHash) { + continue; + } + + $changedFiles[] = $filePath; + } + + $this->fileHashes = $fileHashes; + + foreach (array_keys($oldFileHashes) as $file) { + $deletedFiles[] = $file; + } + + return new FileMonitorResult( + $newFiles, + $changedFiles, + $deletedFiles, + count($fileHashes), + ); + } + + private function getFileHash(string $filePath): string + { + $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 new file mode 100644 index 0000000000..8c7e405dc0 --- /dev/null +++ b/src/File/FileMonitorResult.php @@ -0,0 +1,44 @@ +changedFiles; + } + + public function hasAnyChanges(): bool + { + return count($this->newFiles) > 0 + || count($this->changedFiles) > 0 + || count($this->deletedFiles) > 0; + } + + public function getTotalFilesCount(): int + { + return $this->totalFilesCount; + } + +} diff --git a/src/File/FileReader.php b/src/File/FileReader.php index 5a0c5570fc..7f5a8dc369 100644 --- a/src/File/FileReader.php +++ b/src/File/FileReader.php @@ -3,18 +3,31 @@ namespace PHPStan\File; use function file_get_contents; +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 \PHPStan\File\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 \PHPStan\File\CouldNotReadFileException($fileName); + throw new CouldNotReadFileException($fileName); } return $contents; diff --git a/src/File/FileWriter.php b/src/File/FileWriter.php index 2c92623552..b3659d9536 100644 --- a/src/File/FileWriter.php +++ b/src/File/FileWriter.php @@ -2,7 +2,10 @@ namespace PHPStan\File; -class FileWriter +use function error_get_last; +use function file_put_contents; + +final class FileWriter { public static function write(string $fileName, string $contents): void @@ -11,9 +14,9 @@ public static function write(string $fileName, string $contents): void if ($success === false) { $error = error_get_last(); - throw new \PHPStan\File\CouldNotWriteFileException( + throw new CouldNotWriteFileException( $fileName, - $error !== null ? $error['message'] : 'unknown cause' + $error !== null ? $error['message'] : 'unknown cause', ); } } diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index 4e7e84d644..cf56ee124a 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -2,7 +2,19 @@ namespace PHPStan\File; -class FuzzyRelativePathHelper implements RelativePathHelper +use function count; +use function explode; +use function implode; +use function in_array; +use function ltrim; +use function realpath; +use function str_ends_with; +use function str_starts_with; +use function strlen; +use function substr; +use const DIRECTORY_SEPARATOR; + +final class FuzzyRelativePathHelper implements RelativePathHelper { private string $directorySeparator; @@ -10,14 +22,14 @@ class FuzzyRelativePathHelper implements RelativePathHelper private ?string $pathToTrim = null; /** - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string|null $directorySeparator + * @param non-empty-string|null $directorySeparator */ public function __construct( + private RelativePathHelper $fallbackRelativePathHelper, string $currentWorkingDirectory, array $analysedPaths, - ?string $directorySeparator = null + ?string $directorySeparator = null, ) { if ($directorySeparator === null) { @@ -28,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), @@ -49,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 (\Nette\Utils\Strings::endsWith($pathPart, '.php')) { + if ($i === $pathArraySize - 1 && str_ends_with($pathPart, '.php')) { continue; } if (!isset($pathToTrimArray[$i])) { @@ -96,12 +107,12 @@ 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); } - return $filename; + return $this->fallbackRelativePathHelper->getRelativePath($filename); } } diff --git a/src/File/NullRelativePathHelper.php b/src/File/NullRelativePathHelper.php new file mode 100644 index 0000000000..5e5a07dc62 --- /dev/null +++ b/src/File/NullRelativePathHelper.php @@ -0,0 +1,13 @@ +parentDirectory = $parentDirectory; } public function getRelativePath(string $filename): string { + return implode('/', $this->getFilenameParts($filename)); + } + + /** + * @return string[] + */ + public function getFilenameParts(string $filename): array + { + $schemePosition = strpos($filename, '://'); + if ($schemePosition !== false) { + $filename = substr($filename, $schemePosition + 3); + } $parentParts = explode('/', trim(str_replace('\\', '/', $this->parentDirectory), '/')); $parentPartsCount = count($parentParts); $filenameParts = explode('/', trim(str_replace('\\', '/', $filename), '/')); @@ -37,12 +55,16 @@ public function getRelativePath(string $filename): string } if ($i === 0) { - return $filename; + return [$filename]; } $dotsCount = $parentPartsCount - $i; - return str_repeat('../', $dotsCount) . implode('/', array_slice($filenameParts, $i)); + if ($dotsCount < 0) { + throw new ShouldNotHappenException(); + } + + return array_merge(array_fill(0, $dotsCount, '..'), array_slice($filenameParts, $i)); } } diff --git a/src/File/PathNotFoundException.php b/src/File/PathNotFoundException.php index 185bcf459c..b58cda6e15 100644 --- a/src/File/PathNotFoundException.php +++ b/src/File/PathNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\File; -class PathNotFoundException extends \Exception -{ +use Exception; +use function sprintf; - private string $path; +final class PathNotFoundException extends Exception +{ - public function __construct(string $path) + public function __construct(private string $path) { parent::__construct(sprintf('Path %s does not exist', $path)); - $this->path = $path; } public function getPath(): string diff --git a/src/File/RelativePathHelper.php b/src/File/RelativePathHelper.php index d0dff0ab55..677824591b 100644 --- a/src/File/RelativePathHelper.php +++ b/src/File/RelativePathHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\File; +/** @api */ interface RelativePathHelper { diff --git a/src/File/SimpleRelativePathHelper.php b/src/File/SimpleRelativePathHelper.php index f71341deeb..eff28f5541 100644 --- a/src/File/SimpleRelativePathHelper.php +++ b/src/File/SimpleRelativePathHelper.php @@ -2,23 +2,25 @@ namespace PHPStan\File; -class SimpleRelativePathHelper implements RelativePathHelper -{ +use function str_replace; +use function str_starts_with; +use function strlen; +use function substr; - private string $currentWorkingDirectory; +final class SimpleRelativePathHelper implements RelativePathHelper +{ - public function __construct(string $currentWorkingDirectory) + public function __construct(private string $currentWorkingDirectory) { - $this->currentWorkingDirectory = $currentWorkingDirectory; } public function getRelativePath(string $filename): string { - if ($this->currentWorkingDirectory !== '' && strpos($filename, $this->currentWorkingDirectory) === 0) { - return substr($filename, strlen($this->currentWorkingDirectory) + 1); + if ($this->currentWorkingDirectory !== '' && str_starts_with($filename, $this->currentWorkingDirectory)) { + return str_replace('\\', '/', substr($filename, strlen($this->currentWorkingDirectory) + 1)); } - return $filename; + return str_replace('\\', '/', $filename); } } 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 new file mode 100644 index 0000000000..3279501f51 --- /dev/null +++ b/src/Internal/BytesHelper.php @@ -0,0 +1,31 @@ + $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 58703f78c0..0000000000 --- a/src/Internal/ContainerDynamicReturnTypeExtension.php +++ /dev/null @@ -1,52 +0,0 @@ -getName(), [ - 'getByType', - ], true); - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - if (count($methodCall->args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - $argType = $scope->getType($methodCall->args[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $type = new ObjectType($argType->getValue()); - if ($methodReflection->getName() === 'getByType' && count($methodCall->args) >= 2) { - $argType = $scope->getType($methodCall->args[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 @@ +isInMethodName = $isInMethodName; - $this->removeNullMethodName = $removeNullMethodName; - $this->reflectionProvider = $reflectionProvider; - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->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 - ); - } - -} diff --git a/src/Internal/SprintfHelper.php b/src/Internal/SprintfHelper.php new file mode 100644 index 0000000000..6938c898f1 --- /dev/null +++ b/src/Internal/SprintfHelper.php @@ -0,0 +1,15 @@ +getName() === 'getInternal'; - } - - public function getTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope - ): Type - { - if (count($methodCall->args) < 2) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $getterClosureType = $scope->getType($methodCall->args[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 new file mode 100644 index 0000000000..361177c705 --- /dev/null +++ b/src/Node/BooleanAndNode.php @@ -0,0 +1,47 @@ +getAttributes()); + } + + /** + * @return BooleanAnd|LogicalAnd + */ + public function getOriginalNode() + { + return $this->originalNode; + } + + public function getRightScope(): Scope + { + return $this->rightScope; + } + + public function getType(): string + { + return 'PHPStan_Node_BooleanAndNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/BooleanOrNode.php b/src/Node/BooleanOrNode.php new file mode 100644 index 0000000000..c2ca5d14be --- /dev/null +++ b/src/Node/BooleanOrNode.php @@ -0,0 +1,47 @@ +getAttributes()); + } + + /** + * @return BooleanOr|LogicalOr + */ + public function getOriginalNode() + { + return $this->originalNode; + } + + public function getRightScope(): Scope + { + return $this->rightScope; + } + + public function getType(): string + { + return 'PHPStan_Node_BooleanOrNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/BreaklessWhileLoopNode.php b/src/Node/BreaklessWhileLoopNode.php new file mode 100644 index 0000000000..f7df71bf19 --- /dev/null +++ b/src/Node/BreaklessWhileLoopNode.php @@ -0,0 +1,49 @@ +getAttributes()); + } + + public function getOriginalNode(): While_ + { + return $this->originalNode; + } + + /** + * @return StatementExitPoint[] + */ + public function getExitPoints(): array + { + return $this->exitPoints; + } + + public function getType(): string + { + return 'PHPStan_Node_BreaklessWhileLoop'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/CatchWithUnthrownExceptionNode.php b/src/Node/CatchWithUnthrownExceptionNode.php new file mode 100644 index 0000000000..9f06bf2009 --- /dev/null +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -0,0 +1,48 @@ +getAttributes()); + } + + public function getOriginalNode(): Catch_ + { + return $this->originalNode; + } + + public function getCaughtType(): Type + { + return $this->caughtType; + } + + public function getOriginalCaughtType(): Type + { + return $this->originalCaughtType; + } + + public function getType(): string + { + return 'PHPStan_Node_CatchWithUnthrownExceptionNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php new file mode 100644 index 0000000000..4da543a2d7 --- /dev/null +++ b/src/Node/ClassConstantsNode.php @@ -0,0 +1,65 @@ +getAttributes()); + } + + public function getClass(): ClassLike + { + return $this->class; + } + + /** + * @return ClassConst[] + */ + public function getConstants(): array + { + return $this->constants; + } + + /** + * @return ClassConstantFetch[] + */ + public function getFetches(): array + { + return $this->fetches; + } + + public function getType(): string + { + return 'PHPStan_Node_ClassConstantsNode'; + } + + /** + * @return string[] + */ + 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 new file mode 100644 index 0000000000..4c46fd9253 --- /dev/null +++ b/src/Node/ClassMethodsNode.php @@ -0,0 +1,64 @@ + $methodCalls + */ + public function __construct(private ClassLike $class, private array $methods, private array $methodCalls, private ClassReflection $classReflection) + { + parent::__construct($class->getAttributes()); + } + + public function getClass(): ClassLike + { + return $this->class; + } + + /** + * @return ClassMethod[] + */ + public function getMethods(): array + { + return $this->methods; + } + + /** + * @return array + */ + public function getMethodCalls(): array + { + return $this->methodCalls; + } + + public function getType(): string + { + return 'PHPStan_Node_ClassMethodsNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + +} diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php new file mode 100644 index 0000000000..ec4dcba592 --- /dev/null +++ b/src/Node/ClassPropertiesNode.php @@ -0,0 +1,415 @@ + $propertyUsages + * @param array $methodCalls + * @param array $returnStatementNodes + * @param list $propertyAssigns + */ + 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()); + } + + public function getClass(): ClassLike + { + return $this->class; + } + + /** + * @return ClassPropertyNode[] + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * @return array + */ + public function getPropertyUsages(): array + { + return $this->propertyUsages; + } + + public function getType(): string + { + return 'PHPStan_Node_ClassPropertiesNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + /** + * @param string[] $constructors + * @return array{array, array, array} + */ + public function getUninitializedProperties( + Scope $scope, + array $constructors, + ): array + { + if (!$this->getClass() instanceof Class_) { + return [[], [], []]; + } + $classReflection = $this->getClassReflection(); + + $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; + } + $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; + } + + foreach ($extensions as $extension) { + if (!$extension->isInitialized($propertyReflection, $property->getName())) { + continue; + } + $is = TrinaryLogic::createYes(); + $initializedViaExtension[$property->getName()] = true; + 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 [$uninitializedProperties, [], []]; + } + + $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 = []; + + foreach ($this->getPropertyUsages() as $usage) { + $fetch = $usage->getFetch(); + if (!$fetch instanceof PropertyFetch) { + continue; + } + $usageScope = $usage->getScope(); + if ($usageScope->getFunction() === null) { + continue; + } + $function = $usageScope->getFunction(); + if (!$function instanceof MethodReflection) { + continue; + } + if ($function->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + 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 (TypeUtils::findThisType($fetchedOnType) === null) { + continue; + } + + $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { + continue; + } + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + + if ($usage instanceof PropertyWrite) { + 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->getStartLine(), + $originalProperties[$propertyName], + $usageScope->getFile(), + $usageScope->getFileDescription(), + ]; + } + } + } + + return [ + $this->collectUninitializedProperties(array_keys($methodsCalledFromConstructor), $uninitializedProperties), + $prematureAccess, + $additionalAssigns, + ]; + } + + /** + * @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 + * @param array $initialInitializedProperties + * @param array> $initializedProperties + * @param array $initializedInConstructorProperties + * + * @return array> + */ + private function getMethodsCalledFromConstructor( + ClassReflection $classReflection, + array $initialInitializedProperties, + array $initializedProperties, + array $methods, + array $initializedInConstructorProperties, + ): array + { + $originalMap = $initializedProperties; + $originalMethods = $methods; + + foreach ($this->methodCalls as $methodCall) { + $methodCallNode = $methodCall->getNode(); + if ($methodCallNode instanceof Array_) { + continue; + } + if (!$methodCallNode->name instanceof Identifier) { + continue; + } + $callScope = $methodCall->getScope(); + if ($methodCallNode instanceof Node\Expr\MethodCall) { + $calledOnType = $callScope->getType($methodCallNode->var); + } else { + if (!$methodCallNode->class instanceof Name) { + continue; + } + + $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); + } + + if (TypeUtils::findThisType($calledOnType) === null) { + continue; + } + + $inMethod = $callScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { + continue; + } + 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 (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; + } + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { + continue; + } + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + $initializedProperties[$methodName] = $this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties); + $methods[] = $methodName; + } + + 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 $initialInitializedProperties; + } + + /** + * @return list + */ + public function getPropertyAssigns(): array + { + return $this->propertyAssigns; + } + +} diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php new file mode 100644 index 0000000000..a3eb0567ea --- /dev/null +++ b/src/Node/ClassPropertyNode.php @@ -0,0 +1,187 @@ +getAttributes()); + } + + /** @return non-empty-string */ + public function getName(): string + { + return $this->name; + } + + public function getFlags(): int + { + return $this->flags; + } + + public function getDefault(): ?Expr + { + return $this->default; + } + + 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 & Modifiers::PUBLIC) !== 0 + || ($this->flags & Modifiers::VISIBILITY_MASK) === 0; + } + + public function isProtected(): bool + { + return (bool) ($this->flags & Modifiers::PROTECTED); + } + + public function isPrivate(): bool + { + return (bool) ($this->flags & Modifiers::PRIVATE); + } + + public function isFinal(): bool + { + return (bool) ($this->flags & Modifiers::FINAL); + } + + public function isStatic(): bool + { + return (bool) ($this->flags & Modifiers::STATIC); + } + + public function isReadOnly(): bool + { + 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 Node\Identifier|Node\Name|Node\ComplexType|null + */ + public function getNativeTypeNode() + { + return $this->originalNode->type; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getType(): string + { + return 'PHPStan_Node_ClassPropertyNode'; + } + + /** + * @return string[] + */ + 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 new file mode 100644 index 0000000000..a2bb6889d5 --- /dev/null +++ b/src/Node/ClassStatementsGatherer.php @@ -0,0 +1,299 @@ + */ + private array $propertyUsages = []; + + /** @var Node\Stmt\ClassConst[] */ + private array $constants = []; + + /** @var ClassConstantFetch[] */ + private array $constantFetches = []; + + /** @var array */ + private array $returnStatementNodes = []; + + /** @var list */ + private array $propertyAssigns = []; + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function __construct( + private ClassReflection $classReflection, + callable $nodeCallback, + ) + { + $this->nodeCallback = $nodeCallback; + } + + /** + * @return ClassPropertyNode[] + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * @return ClassMethod[] + */ + public function getMethods(): array + { + return $this->methods; + } + + /** + * @return Method\MethodCall[] + */ + public function getMethodCalls(): array + { + return $this->methodCalls; + } + + /** + * @return array + */ + public function getPropertyUsages(): array + { + return $this->propertyUsages; + } + + /** + * @return Node\Stmt\ClassConst[] + */ + public function getConstants(): array + { + return $this->constants; + } + + /** + * @return ClassConstantFetch[] + */ + 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; + $nodeCallback($node, $scope); + $this->gatherNodes($node, $scope); + } + + private function gatherNodes(Node $node, Scope $scope): void + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) { + return; + } + 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) { + $this->methods[] = new ClassMethod($node, $scope->isInTrait()); + return; + } + if ($node instanceof Node\Stmt\ClassConst) { + $this->constants[] = $node; + return; + } + 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; + } + if ($node instanceof Expr\ClassConstFetch) { + $this->constantFetches[] = new ClassConstantFetch($node, $scope); + return; + } + if ($node instanceof PropertyAssignNode) { + $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false); + $this->propertyAssigns[] = new PropertyAssign($node, $scope); + return; + } + if (!$node instanceof Expr) { + return; + } + if ($node instanceof Expr\AssignOp\Coalesce) { + $this->gatherNodes($node->var, $scope); + return; + } + if ($node instanceof Expr\AssignRef) { + if (!$node->expr instanceof PropertyFetch && !$node->expr instanceof StaticPropertyFetch) { + $this->gatherNodes($node->expr, $scope); + return; + } + + $this->propertyUsages[] = new PropertyRead($node->expr, $scope); + $this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false); + return; + } + if ($node instanceof FunctionCallableNode) { + $node = $node->getOriginalNode(); + } elseif ($node instanceof InstantiationCallableNode) { + $node = $node->getOriginalNode(); + } + + $inAssign = $scope->isInExpressionAssign($node); + if ($inAssign) { + return; + } + + while ($node instanceof ArrayDimFetch) { + $node = $node->var; + } + if (!$node instanceof PropertyFetch && !$node instanceof StaticPropertyFetch) { + return; + } + + $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 329994ad0c..920b61750b 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -2,35 +2,40 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use function count; -class ClosureReturnStatementsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - private \PhpParser\Node\Expr\Closure $closureExpr; - - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; + private Node\Expr\Closure $closureExpr; /** - * @param \PhpParser\Node\Expr\Closure $closureExpr - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( Closure $closureExpr, - array $returnStatements, - StatementResult $statementResult + private array $returnStatements, + private array $yieldStatements, + private StatementResult $statementResult, + private array $executionEnds, + private array $impurePoints, ) { parent::__construct($closureExpr->getAttributes()); $this->closureExpr = $closureExpr; - $this->returnStatements = $returnStatements; - $this->statementResult = $statementResult; } public function getClosureExpr(): Closure @@ -38,19 +43,46 @@ public function getClosureExpr(): Closure return $this->closureExpr; } - /** - * @return \PHPStan\Node\ReturnStatement[] - */ + public function hasNativeReturnTypehint(): bool + { + return $this->closureExpr->returnType !== null; + } + public function getReturnStatements(): array { return $this->returnStatements; } + 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; } + public function returnsByRef(): bool + { + return $this->closureExpr->byRef; + } + public function getType(): string { return 'PHPStan_Node_ClosureReturnStatementsNode'; 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 new file mode 100644 index 0000000000..bda533900b --- /dev/null +++ b/src/Node/Constant/ClassConstantFetch.php @@ -0,0 +1,28 @@ +node; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php new file mode 100644 index 0000000000..89f6c7f4bf --- /dev/null +++ b/src/Node/DoWhileLoopConditionNode.php @@ -0,0 +1,46 @@ +getAttributes()); + } + + public function getCond(): Expr + { + return $this->cond; + } + + /** + * @return StatementExitPoint[] + */ + public function getExitPoints(): array + { + return $this->exitPoints; + } + + public function getType(): string + { + return 'PHPStan_Node_ClosureReturnStatementsNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/ExecutionEndNode.php b/src/Node/ExecutionEndNode.php index d710f04a7e..5e0ddf13da 100644 --- a/src/Node/ExecutionEndNode.php +++ b/src/Node/ExecutionEndNode.php @@ -6,28 +6,22 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementResult; -class ExecutionEndNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class ExecutionEndNode extends NodeAbstract implements VirtualNode { - private Node $node; - - private StatementResult $statementResult; - - private bool $hasNativeReturnTypehint; - public function __construct( - Node $node, - StatementResult $statementResult, - bool $hasNativeReturnTypehint + private Node\Stmt $node, + private StatementResult $statementResult, + private bool $hasNativeReturnTypehint, ) { parent::__construct($node->getAttributes()); - $this->node = $node; - $this->statementResult = $statementResult; - $this->hasNativeReturnTypehint = $hasNativeReturnTypehint; } - 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 new file mode 100644 index 0000000000..43d3a19a09 --- /dev/null +++ b/src/Node/Expr/GetIterableValueTypeExpr.php @@ -0,0 +1,34 @@ +expr; + } + + public function getType(): string + { + return 'PHPStan_Node_GetIterableValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/GetOffsetValueTypeExpr.php b/src/Node/Expr/GetOffsetValueTypeExpr.php new file mode 100644 index 0000000000..6bb047212a --- /dev/null +++ b/src/Node/Expr/GetOffsetValueTypeExpr.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_GetOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/OriginalPropertyTypeExpr.php b/src/Node/Expr/OriginalPropertyTypeExpr.php new file mode 100644 index 0000000000..b04990efdc --- /dev/null +++ b/src/Node/Expr/OriginalPropertyTypeExpr.php @@ -0,0 +1,34 @@ +propertyFetch; + } + + public function getType(): string + { + return 'PHPStan_Node_OriginalPropertyTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/ParameterVariableOriginalValueExpr.php b/src/Node/Expr/ParameterVariableOriginalValueExpr.php new file mode 100644 index 0000000000..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 new file mode 100644 index 0000000000..ed6ddae657 --- /dev/null +++ b/src/Node/Expr/SetOffsetValueTypeExpr.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_SetOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} 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 d8f9c0d80f..286168fb6a 100644 --- a/src/Node/FileNode.php +++ b/src/Node/FileNode.php @@ -2,26 +2,26 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\NodeAbstract; -class FileNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class FileNode extends NodeAbstract implements VirtualNode { - /** @var \PhpParser\Node[] */ - private array $nodes; - /** - * @param \PhpParser\Node[] $nodes + * @param Node[] $nodes */ - public function __construct(array $nodes) + public function __construct(private array $nodes) { $firstNode = $nodes[0] ?? null; parent::__construct($firstNode !== null ? $firstNode->getAttributes() : []); - $this->nodes = $nodes; } /** - * @return \PhpParser\Node[] + * @return Node[] */ public function getNodes(): array { diff --git a/src/Node/FinallyExitPointsNode.php b/src/Node/FinallyExitPointsNode.php new file mode 100644 index 0000000000..fed8d4888d --- /dev/null +++ b/src/Node/FinallyExitPointsNode.php @@ -0,0 +1,52 @@ +finallyExitPoints; + } + + /** + * @return StatementExitPoint[] + */ + public function getTryCatchExitPoints(): array + { + return $this->tryCatchExitPoints; + } + + public function getType(): string + { + return 'PHPStan_Node_FinallyExitPointsNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FunctionCallableNode.php b/src/Node/FunctionCallableNode.php new file mode 100644 index 0000000000..9cd2cfc8c8 --- /dev/null +++ b/src/Node/FunctionCallableNode.php @@ -0,0 +1,45 @@ +originalNode->getAttributes()); + } + + /** + * @return Expr|Name + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\FuncCall + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_FunctionCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index ffadd3007a..14582f309b 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -2,37 +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; -class FunctionReturnStatementsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; - /** - * @param \PhpParser\Node\Stmt\Function_ $function - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( - Function_ $function, - array $returnStatements, - StatementResult $statementResult + 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()); - $this->returnStatements = $returnStatements; - $this->statementResult = $statementResult; } - /** - * @return \PHPStan\Node\ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -43,6 +47,36 @@ public function getStatementResult(): StatementResult return $this->statementResult; } + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function returnsByRef(): bool + { + return $this->function->byRef; + } + + 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'; @@ -56,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 9fb926e785..20acb7c36f 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -2,21 +2,31 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; -class InArrowFunctionNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InArrowFunctionNode extends NodeAbstract implements VirtualNode { - private \PhpParser\Node\Expr\ArrowFunction $originalNode; + 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 getOriginalNode(): \PhpParser\Node\Expr\ArrowFunction + 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 116892703f..52b50c17fd 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -2,18 +2,36 @@ namespace PHPStan\Node; -class InClassMethodNode extends \PhpParser\Node\Stmt implements VirtualNode +use PhpParser\Node; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; + +/** + * @api + */ +final class InClassMethodNode extends Node\Stmt implements VirtualNode { - private \PhpParser\Node\Stmt\ClassMethod $originalNode; - - public function __construct(\PhpParser\Node\Stmt\ClassMethod $originalNode) + public function __construct( + private ClassReflection $classReflection, + private PhpMethodFromParserNodeReflection $methodReflection, + private Node\Stmt\ClassMethod $originalNode, + ) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; } - public function getOriginalNode(): \PhpParser\Node\Stmt\ClassMethod + 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 a45216d1c8..84a4ecab83 100644 --- a/src/Node/InClassNode.php +++ b/src/Node/InClassNode.php @@ -2,21 +2,19 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; use PHPStan\Reflection\ClassReflection; -class InClassNode extends \PhpParser\Node\Stmt implements VirtualNode +/** + * @api + */ +final class InClassNode extends Node\Stmt implements VirtualNode { - private ClassLike $originalNode; - - private ClassReflection $classReflection; - - public function __construct(ClassLike $originalNode, ClassReflection $classReflection) + public function __construct(private ClassLike $originalNode, private ClassReflection $classReflection) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->classReflection = $classReflection; } public function getOriginalNode(): ClassLike diff --git a/src/Node/InClosureNode.php b/src/Node/InClosureNode.php index 8d08a3ffa1..3e95aea867 100644 --- a/src/Node/InClosureNode.php +++ b/src/Node/InClosureNode.php @@ -2,20 +2,30 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; -class InClosureNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class InClosureNode extends NodeAbstract implements VirtualNode { - private \PhpParser\Node\Expr\Closure $originalNode; + 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 new file mode 100644 index 0000000000..4e474c98b2 --- /dev/null +++ b/src/Node/InForeachNode.php @@ -0,0 +1,34 @@ +getAttributes()); + } + + public function getOriginalNode(): Foreach_ + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_InForeachNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InFunctionNode.php b/src/Node/InFunctionNode.php index 632c20044b..ce90bb7f38 100644 --- a/src/Node/InFunctionNode.php +++ b/src/Node/InFunctionNode.php @@ -2,18 +2,29 @@ namespace PHPStan\Node; -class InFunctionNode extends \PhpParser\Node\Stmt implements VirtualNode -{ +use PhpParser\Node; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; - private \PhpParser\Node\Stmt\Function_ $originalNode; +/** + * @api + */ +final class InFunctionNode extends Node\Stmt implements VirtualNode +{ - public function __construct(\PhpParser\Node\Stmt\Function_ $originalNode) + public function __construct( + private PhpFunctionFromParserNodeReflection $functionReflection, + private Node\Stmt\Function_ $originalNode, + ) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; } - public function getOriginalNode(): \PhpParser\Node\Stmt\Function_ + 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 new file mode 100644 index 0000000000..98d838b1be --- /dev/null +++ b/src/Node/InstantiationCallableNode.php @@ -0,0 +1,45 @@ +originalNode->getAttributes()); + } + + /** + * @return Expr|Name + */ + public function getClass() + { + return $this->class; + } + + public function getOriginalNode(): Expr\New_ + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_InstantiationCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InvalidateExprNode.php b/src/Node/InvalidateExprNode.php new file mode 100644 index 0000000000..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 9c06351f09..4d9699121b 100644 --- a/src/Node/LiteralArrayItem.php +++ b/src/Node/LiteralArrayItem.php @@ -2,20 +2,17 @@ namespace PHPStan\Node; -use PhpParser\Node\Expr\ArrayItem; +use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; -class LiteralArrayItem +/** + * @api + */ +final class LiteralArrayItem { - private Scope $scope; - - private ArrayItem $arrayItem; - - public function __construct(Scope $scope, ArrayItem $arrayItem) + public function __construct(private Scope $scope, private ?ArrayItem $arrayItem) { - $this->scope = $scope; - $this->arrayItem = $arrayItem; } public function getScope(): Scope @@ -23,7 +20,7 @@ public function getScope(): Scope return $this->scope; } - public function getArrayItem(): ArrayItem + public function getArrayItem(): ?ArrayItem { return $this->arrayItem; } diff --git a/src/Node/LiteralArrayNode.php b/src/Node/LiteralArrayNode.php index 17528158da..9c8a693ff6 100644 --- a/src/Node/LiteralArrayNode.php +++ b/src/Node/LiteralArrayNode.php @@ -5,20 +5,18 @@ use PhpParser\Node\Expr\Array_; use PhpParser\NodeAbstract; -class LiteralArrayNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class LiteralArrayNode extends NodeAbstract implements VirtualNode { - /** @var LiteralArrayItem[] */ - private array $itemNodes; - /** - * @param Array_ $originalNode * @param LiteralArrayItem[] $itemNodes */ - public function __construct(Array_ $originalNode, array $itemNodes) + public function __construct(Array_ $originalNode, private array $itemNodes) { parent::__construct($originalNode->getAttributes()); - $this->itemNodes = $itemNodes; } /** diff --git a/src/Node/MatchExpressionArm.php b/src/Node/MatchExpressionArm.php new file mode 100644 index 0000000000..bad6265698 --- /dev/null +++ b/src/Node/MatchExpressionArm.php @@ -0,0 +1,36 @@ +body; + } + + /** + * @return MatchExpressionArmCondition[] + */ + public function getConditions(): array + { + return $this->conditions; + } + + public function getLine(): int + { + return $this->line; + } + +} 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 new file mode 100644 index 0000000000..95a291cce3 --- /dev/null +++ b/src/Node/MatchExpressionArmCondition.php @@ -0,0 +1,33 @@ +condition; + } + + public function getScope(): Scope + { + return $this->scope; + } + + public function getLine(): int + { + return $this->line; + } + +} diff --git a/src/Node/MatchExpressionNode.php b/src/Node/MatchExpressionNode.php new file mode 100644 index 0000000000..fb7f360642 --- /dev/null +++ b/src/Node/MatchExpressionNode.php @@ -0,0 +1,59 @@ +getAttributes()); + } + + public function getCondition(): Expr + { + return $this->condition; + } + + /** + * @return MatchExpressionArm[] + */ + public function getArms(): array + { + return $this->arms; + } + + public function getEndScope(): Scope + { + return $this->endScope; + } + + public function getType(): string + { + return 'PHPStan_Node_MatchExpression'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Method/MethodCall.php b/src/Node/Method/MethodCall.php new file mode 100644 index 0000000000..d3726915a9 --- /dev/null +++ b/src/Node/Method/MethodCall.php @@ -0,0 +1,36 @@ +node; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php new file mode 100644 index 0000000000..b1e52bf1e6 --- /dev/null +++ b/src/Node/MethodCallableNode.php @@ -0,0 +1,54 @@ +getAttributes()); + } + + public function getVar(): Expr + { + return $this->var; + } + + /** + * @return Expr|Identifier + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\MethodCall + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_MethodCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index dd6f833953..2444df30b5 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -2,37 +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; -class MethodReturnStatementsNode extends NodeAbstract implements VirtualNode +/** + * @api + */ +final class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; + private ClassMethod $classMethod; /** - * @param \PhpParser\Node\Stmt\ClassMethod $method - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( ClassMethod $method, - array $returnStatements, - StatementResult $statementResult + 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->returnStatements = $returnStatements; - $this->statementResult = $statementResult; + $this->classMethod = $method; } - /** - * @return \PHPStan\Node\ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -43,9 +52,67 @@ public function getStatementResult(): StatementResult return $this->statementResult; } + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function returnsByRef(): bool + { + return $this->classMethod->byRef; + } + + 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 new file mode 100644 index 0000000000..86c220b77f --- /dev/null +++ b/src/Node/Property/PropertyRead.php @@ -0,0 +1,35 @@ +fetch; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/src/Node/Property/PropertyWrite.php b/src/Node/Property/PropertyWrite.php new file mode 100644 index 0000000000..df39b83d0b --- /dev/null +++ b/src/Node/Property/PropertyWrite.php @@ -0,0 +1,37 @@ +fetch; + } + + 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 new file mode 100644 index 0000000000..7537b8935d --- /dev/null +++ b/src/Node/PropertyAssignNode.php @@ -0,0 +1,48 @@ +getAttributes()); + } + + public function getPropertyFetch(): Expr\PropertyFetch|Expr\StaticPropertyFetch + { + return $this->propertyFetch; + } + + public function getAssignedExpr(): Expr + { + return $this->assignedExpr; + } + + public function isAssignOp(): bool + { + return $this->assignOp; + } + + public function getType(): string + { + return 'PHPStan_Node_PropertyAssignNodeNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/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 4ea6266cfb..153faf1534 100644 --- a/src/Node/ReturnStatement.php +++ b/src/Node/ReturnStatement.php @@ -2,19 +2,20 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -class ReturnStatement +/** + * @api + */ +final class ReturnStatement { - private Scope $scope; + private Node\Stmt\Return_ $returnNode; - private \PhpParser\Node\Stmt\Return_ $returnNode; - - public function __construct(Scope $scope, Return_ $returnNode) + public function __construct(private Scope $scope, Return_ $returnNode) { - $this->scope = $scope; $this->returnNode = $returnNode; } diff --git a/src/Node/ReturnStatementsNode.php b/src/Node/ReturnStatementsNode.php new file mode 100644 index 0000000000..34c28ef538 --- /dev/null +++ b/src/Node/ReturnStatementsNode.php @@ -0,0 +1,42 @@ + + */ + 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 new file mode 100644 index 0000000000..407a5cfa4c --- /dev/null +++ b/src/Node/StaticMethodCallableNode.php @@ -0,0 +1,58 @@ +getAttributes()); + } + + /** + * @return Expr|Name + */ + public function getClass() + { + return $this->class; + } + + /** + * @return Identifier|Expr + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\StaticCall + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_StaticMethodCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/UnreachableStatementNode.php b/src/Node/UnreachableStatementNode.php index 3b92895b8e..3e2c72e29b 100644 --- a/src/Node/UnreachableStatementNode.php +++ b/src/Node/UnreachableStatementNode.php @@ -4,15 +4,16 @@ use PhpParser\Node\Stmt; -class UnreachableStatementNode extends Stmt implements VirtualNode +/** + * @api + */ +final class UnreachableStatementNode extends Stmt implements VirtualNode { - private Stmt $originalStatement; - - public function __construct(Stmt $originalStatement) + /** @param Stmt[] $nextStatements */ + public function __construct(private Stmt $originalStatement, private array $nextStatements = []) { parent::__construct($originalStatement->getAttributes()); - $this->originalStatement = $originalStatement; } public function getOriginalStatement(): Stmt @@ -33,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/Node/VirtualNode.php b/src/Node/VirtualNode.php index fcb077279a..fe46a1551d 100644 --- a/src/Node/VirtualNode.php +++ b/src/Node/VirtualNode.php @@ -4,6 +4,7 @@ use PhpParser\Node; +/** @api */ interface VirtualNode extends Node { diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index f407303a4e..96dcd36820 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -2,67 +2,124 @@ namespace PHPStan\Parallel; +use Closure; use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use Nette\Utils\Random; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; -use PHPStan\Command\AnalyseCommand; -use React\EventLoop\StreamSelectLoop; +use PHPStan\Analyser\InternalError; +use PHPStan\Dependency\RootExportedNode; +use PHPStan\Process\ProcessHelper; +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; -use function escapeshellarg; +use Throwable; +use function array_map; +use function array_pop; +use function array_reverse; +use function array_sum; +use function count; +use function defined; +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 int $internalErrorsCountLimit; + private const DEFAULT_TIMEOUT = 600.0; private float $processTimeout; private ProcessPool $processPool; - private int $decoderBufferSize; - public function __construct( - int $internalErrorsCountLimit, + private int $internalErrorsCountLimit, float $processTimeout, - int $decoderBufferSize + private int $decoderBufferSize, ) { - $this->internalErrorsCountLimit = $internalErrorsCountLimit; - $this->processTimeout = $processTimeout; - $this->decoderBufferSize = $decoderBufferSize; + $this->processTimeout = max($processTimeout, self::DEFAULT_TIMEOUT); } /** - * @param Schedule $schedule - * @param string $mainScript - * @param \Closure(int): void|null $postFileCallback - * @param string|null $projectConfigFile - * @return AnalyserResult + * @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, + ?Closure $postFileCallback, ?string $projectConfigFile, - InputInterface $input - ): AnalyserResult + InputInterface $input, + ?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, 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++; + } - $server = new \React\Socket\TcpServer('127.0.0.1:0', $loop); - $this->processPool = new ProcessPool($server); + $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 { - $decoder = new Decoder($connection, true, 512, 0, $this->decoderBufferSize); - $encoder = new Encoder($connection); + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, $this->decoderBufferSize); + $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $decoder->on('data', function (array $data) use (&$jobs, $decoder, $encoder): void { if ($data['action'] !== 'hello') { return; @@ -72,7 +129,7 @@ public function analyse( $process = $this->processPool->getProcess($identifier); $process->bindConnection($decoder, $encoder); if (count($jobs) === 0) { - $this->processPool->quitProcess($identifier); + $this->processPool->tryQuitProcess($identifier); return; } @@ -83,43 +140,82 @@ 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; - - // todo should probably differentiate between not showing unmatched ignores + showing "Internal error limit reached..." - $reachedInternalErrorsCountLimit = false; - - $handleError = function (\Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { - $internalErrors[] = sprintf('Internal error: ' . $error->getMessage()); + $handleError = function (Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { + $internalErrors[] = new InternalError( + $error->getMessage(), + 'communicating with parallel worker', + InternalError::prepareTrace($error), + $error->getTraceAsString(), + !$error instanceof ProcessTimedOutException, + ); $internalErrorsCount++; $reachedInternalErrorsCountLimit = true; $this->processPool->quitAll(); }; - $dependencies = []; for ($i = 0; $i < $numberOfProcesses; $i++) { if (count($jobs) === 0) { break; } $processIdentifier = Random::generate(); - $process = new Process($this->getWorkerCommand( + $commandOptions = [ + '--port', + (string) $serverPort, + '--identifier', + $processIdentifier, + ]; + + $process = new Process(ProcessHelper::getWorkerCommand( $mainScript, + 'worker', $projectConfigFile, - $serverPort, - $processIdentifier, - $input + $commandOptions, + $input, ), $loop, $this->processTimeout); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$dependencies, &$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); + } - $errors[] = Error::decode($jsonError); + foreach ($json['allPhpErrors'] as $allPhpError) { + $allPhpErrors[] = Error::decode($allPhpError); + } + + $locallyIgnoredFileErrors = []; + foreach ($json['locallyIgnoredErrors'] as $locallyIgnoredJsonError) { + $locallyIgnoredFileErrors[] = Error::decode($locallyIgnoredJsonError); + } + + if ($onFileAnalysisHandler !== null) { + $onFileAnalysisHandler($fileErrors, $locallyIgnoredFileErrors, $json['files']); + } + + foreach ($fileErrors as $fileError) { + $errors[] = $fileError; + } + + foreach ($locallyIgnoredFileErrors as $locallyIgnoredFileError) { + $locallyIgnoredErrors[] = $locallyIgnoredFileError; + } + + foreach ($json['collectedData'] as $file => $jsonDataByCollector) { + foreach ($jsonDataByCollector as $collectorType => $listOfCollectedData) { + foreach ($listOfCollectedData as $rawCollectedData) { + $collectedData[$file][$collectorType][] = $rawCollectedData; + } + } } /** @@ -130,8 +226,41 @@ 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 + */ + foreach ($json['exportedNodes'] as $file => $fileExportedNodes) { + if (count($fileExportedNodes) === 0) { + continue; + } + $exportedNodes[$file] = array_map(static function (array $node): RootExportedNode { + $class = $node['type']; + + return $class::decode($node['data']); + }, $fileExportedNodes); + } + 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']; @@ -141,98 +270,56 @@ public function analyse( } if (count($jobs) === 0) { - $this->processPool->quitProcess($processIdentifier); + $this->processPool->tryQuitProcess($processIdentifier); return; } $job = array_pop($jobs); $process->request(['action' => 'analyse', 'files' => $job]); - }, $handleError, function ($exitCode, string $output) use (&$internalErrors, $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); - }); - $this->processPool->attachProcess($processIdentifier, $process); - } - - $loop->run(); - - return new AnalyserResult( - $errors, - $internalErrors, - $internalErrorsCount === 0 ? $dependencies : null, - $reachedInternalErrorsCountLimit - ); - } - - private function getWorkerCommand( - string $mainScript, - ?string $projectConfigFile, - int $port, - string $identifier, - InputInterface $input - ): string - { - $processCommandArray = [ - escapeshellarg(PHP_BINARY), - ]; - - if ($input->getOption('memory-limit') === null) { - $processCommandArray[] = '-d'; - $processCommandArray[] = 'memory_limit=' . ini_get('memory_limit'); - } - - foreach ([$mainScript, 'worker'] as $arg) { - $processCommandArray[] = escapeshellarg($arg); - } - - if ($projectConfigFile !== null) { - $processCommandArray[] = '--configuration'; - $processCommandArray[] = escapeshellarg($projectConfigFile); - } - - $options = [ - 'paths-file', - AnalyseCommand::OPTION_LEVEL, - 'autoload-file', - 'memory-limit', - 'xdebug', - ]; - foreach ($options as $optionName) { - /** @var bool|string|null $optionValue */ - $optionValue = $input->getOption($optionName); - if (is_bool($optionValue)) { - if ($optionValue === true) { - $processCommandArray[] = sprintf('--%s', $optionName); - } - continue; - } - if ($optionValue === null) { - continue; - } - - $processCommandArray[] = sprintf('--%s=%s', $optionName, escapeshellarg($optionValue)); - } + $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; + } - $processCommandArray[] = sprintf('--port'); - $processCommandArray[] = $port; - - $processCommandArray[] = sprintf('--identifier'); - $processCommandArray[] = escapeshellarg($identifier); + $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; + } - /** @var string[] $paths */ - $paths = $input->getArgument('paths'); - foreach ($paths as $path) { - $processCommandArray[] = escapeshellarg($path); + $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); } - return implode(' ', $processCommandArray); + return $deferred->promise(); } } diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index 093d19c5a3..e5cf90566f 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -2,23 +2,25 @@ namespace PHPStan\Parallel; +use PHPStan\ShouldNotHappenException; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; - -class Process +use Throwable; +use function fclose; +use function is_string; +use function rewind; +use function sprintf; +use function stream_get_contents; +use function tmpfile; + +final class Process { - private string $command; - public \React\ChildProcess\Process $process; - private LoopInterface $loop; - - private float $timeoutSeconds; - - private WritableStreamInterface $in; + private ?WritableStreamInterface $in = null; /** @var resource */ private $stdOut; @@ -26,34 +28,36 @@ class Process /** @var resource */ private $stdErr; - /** @var callable */ + /** @var callable(mixed[] $json) : void */ private $onData; - /** @var callable */ + /** @var callable(Throwable $exception): void */ private $onError; private ?TimerInterface $timer = null; public function __construct( - string $command, - LoopInterface $loop, - float $timeoutSeconds + private string $command, + private LoopInterface $loop, + private float $timeoutSeconds, ) { - $this->command = $command; - $this->loop = $loop; - $this->timeoutSeconds = $timeoutSeconds; } + /** + * @param callable(mixed[] $json) : void $onData + * @param callable(Throwable $exception): void $onError + * @param callable(?int $exitCode, string $output) : void $onExit + */ public function start(callable $onData, callable $onError, callable $onExit): void { $tmpStdOut = tmpfile(); if ($tmpStdOut === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stdout.'); + throw new ShouldNotHappenException('Failed creating temp file for stdout.'); } $tmpStdErr = tmpfile(); if ($tmpStdErr === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stderr.'); + throw new ShouldNotHappenException('Failed creating temp file for stderr.'); } $this->stdOut = $tmpStdOut; $this->stdErr = $tmpStdErr; @@ -101,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))); }); } @@ -119,17 +126,17 @@ public function quit(): void $pipe->close(); } - // todo should I close/end something here or is it enough to terminate the process? - $this->in->end(); + if ($this->in === null) { + return; + } - // todo what are all the events I should listen to? - // process: just exit now - // connection: connection, data, error? + $this->in->end(); } public function bindConnection(ReadableStreamInterface $out, WritableStreamInterface $in): void { $out->on('data', function (array $json): void { + $this->cancelTimer(); if ($json['action'] !== 'result') { return; } @@ -138,7 +145,11 @@ public function bindConnection(ReadableStreamInterface $out, WritableStreamInter $onData($json['result']); }); $this->in = $in; - $out->on('error', function (\Throwable $error): void { + $out->on('error', function (Throwable $error): void { + $onError = $this->onError; + $onError($error); + }); + $in->on('error', function (Throwable $error): void { $onError = $this->onError; $onError($error); }); diff --git a/src/Parallel/ProcessPool.php b/src/Parallel/ProcessPool.php index 20c293ba73..574cabcf6e 100644 --- a/src/Parallel/ProcessPool.php +++ b/src/Parallel/ProcessPool.php @@ -2,26 +2,34 @@ namespace PHPStan\Parallel; +use PHPStan\ShouldNotHappenException; use React\Socket\TcpServer; use function array_key_exists; +use function array_keys; +use function count; +use function sprintf; -class ProcessPool +final class ProcessPool { - private TcpServer $server; - /** @var array */ private array $processes = []; - public function __construct(TcpServer $server) + /** @var callable(): void */ + private $onServerClose; + + /** + * @param callable(): void $onServerClose + */ + public function __construct(private TcpServer $server, callable $onServerClose) { - $this->server = $server; + $this->onServerClose = $onServerClose; } public function getProcess(string $identifier): Process { if (!array_key_exists($identifier, $this->processes)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Process %s not found.', $identifier)); + throw new ShouldNotHappenException(sprintf('Process %s not found.', $identifier)); } return $this->processes[$identifier]; @@ -41,7 +49,7 @@ public function tryQuitProcess(string $identifier): void $this->quitProcess($identifier); } - public function quitProcess(string $identifier): void + private function quitProcess(string $identifier): void { $process = $this->getProcess($identifier); $process->quit(); @@ -51,6 +59,8 @@ public 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 @@ +> */ - private array $jobs; - /** - * @param int $numberOfProcesses * @param array> $jobs */ - public function __construct(int $numberOfProcesses, array $jobs) + public function __construct(private int $numberOfProcesses, private array $jobs) { - $this->numberOfProcesses = $numberOfProcesses; - $this->jobs = $jobs; } public function getNumberOfProcesses(): int diff --git a/src/Parallel/Scheduler.php b/src/Parallel/Scheduler.php index 37e00fd87b..45ad9086f1 100644 --- a/src/Parallel/Scheduler.php +++ b/src/Parallel/Scheduler.php @@ -2,43 +2,73 @@ namespace PHPStan\Parallel; -class Scheduler -{ - - private int $jobSize; +use PHPStan\Command\Output; +use PHPStan\Diagnose\DiagnoseExtension; +use function array_chunk; +use function count; +use function floor; +use function max; +use function min; +use function sprintf; - private int $maximumNumberOfProcesses; +final class Scheduler implements DiagnoseExtension +{ - private int $minimumNumberOfJobsPerProcess; + /** @var array{int, int, int, int}|null */ + private ?array $storedData = null; + /** + * @param positive-int $jobSize + * @param positive-int $maximumNumberOfProcesses + * @param positive-int $minimumNumberOfJobsPerProcess + */ public function __construct( - int $jobSize, - int $maximumNumberOfProcesses, - int $minimumNumberOfJobsPerProcess + private int $jobSize, + private int $maximumNumberOfProcesses, + private int $minimumNumberOfJobsPerProcess, ) { - $this->jobSize = $jobSize; - $this->maximumNumberOfProcesses = $maximumNumberOfProcesses; - $this->minimumNumberOfJobsPerProcess = $minimumNumberOfJobsPerProcess; } /** - * @param int $cpuCores * @param array $files - * @return Schedule */ public function scheduleWork( int $cpuCores, - array $files + array $files, ): Schedule { $jobs = array_chunk($files, $this->jobSize); $numberOfProcesses = min( - (int) floor(count($jobs) / $this->minimumNumberOfJobsPerProcess), - $cpuCores + max((int) floor(count($jobs) / $this->minimumNumberOfJobsPerProcess), 1), + $cpuCores, ); - return new Schedule(min($numberOfProcesses, $this->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 1185387610..62bba62a2b 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -2,64 +2,57 @@ namespace PHPStan\Parser; -class CachedParser implements Parser -{ - - private \PHPStan\Parser\Parser $originalParser; - - /** @var array */ - private array $cachedNodesByFile = []; +use PhpParser\Node; +use PHPStan\File\FileReader; +use function array_slice; - private int $cachedNodesByFileCount = 0; - - private int $cachedNodesByFileCountMax; +final class CachedParser implements Parser +{ - /** @var array*/ + /** @var array*/ private array $cachedNodesByString = []; private int $cachedNodesByStringCount = 0; - private int $cachedNodesByStringCountMax; + /** @var array */ + private array $parsedByString = []; public function __construct( - Parser $originalParser, - int $cachedNodesByFileCountMax, - int $cachedNodesByStringCountMax + private Parser $originalParser, + private int $cachedNodesByStringCountMax, ) { - $this->originalParser = $originalParser; - $this->cachedNodesByFileCountMax = $cachedNodesByFileCountMax; - $this->cachedNodesByStringCountMax = $cachedNodesByStringCountMax; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { - if ($this->cachedNodesByFileCountMax !== 0 && $this->cachedNodesByFileCount >= $this->cachedNodesByFileCountMax) { - $this->cachedNodesByFile = array_slice( - $this->cachedNodesByFile, + if ($this->cachedNodesByStringCountMax !== 0 && $this->cachedNodesByStringCount >= $this->cachedNodesByStringCountMax) { + $this->cachedNodesByString = array_slice( + $this->cachedNodesByString, 1, null, - true + true, ); - --$this->cachedNodesByFileCount; + --$this->cachedNodesByStringCount; } - if (!isset($this->cachedNodesByFile[$file])) { - $this->cachedNodesByFile[$file] = $this->originalParser->parseFile($file); - $this->cachedNodesByFileCount++; + $sourceCode = FileReader::read($file); + if (!isset($this->cachedNodesByString[$sourceCode]) || isset($this->parsedByString[$sourceCode])) { + $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseFile($file); + $this->cachedNodesByStringCount++; + unset($this->parsedByString[$sourceCode]); } - return $this->cachedNodesByFile[$file]; + return $this->cachedNodesByString[$sourceCode]; } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { @@ -68,7 +61,7 @@ public function parseString(string $sourceCode): array $this->cachedNodesByString, 1, null, - true + true, ); --$this->cachedNodesByStringCount; @@ -77,21 +70,12 @@ public function parseString(string $sourceCode): array if (!isset($this->cachedNodesByString[$sourceCode])) { $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseString($sourceCode); $this->cachedNodesByStringCount++; + $this->parsedByString[$sourceCode] = true; } return $this->cachedNodesByString[$sourceCode]; } - public function getCachedNodesByFileCount(): int - { - return $this->cachedNodesByFileCount; - } - - public function getCachedNodesByFileCountMax(): int - { - return $this->cachedNodesByFileCountMax; - } - public function getCachedNodesByStringCount(): int { return $this->cachedNodesByStringCount; @@ -103,15 +87,7 @@ public function getCachedNodesByStringCountMax(): int } /** - * @return array - */ - public function getCachedNodesByFile(): array - { - return $this->cachedNodesByFile; - } - - /** - * @return array + * @return array */ public function getCachedNodesByString(): array { diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php new file mode 100644 index 0000000000..0f874eafbf --- /dev/null +++ b/src/Parser/CleaningParser.php @@ -0,0 +1,41 @@ +traverser = new NodeTraverser(); + $this->traverser->addVisitor(new CleaningVisitor()); + $this->traverser->addVisitor(new RemoveUnusedCodeByPhpVersionIdVisitor($phpVersion->getVersionString())); + } + + public function parseFile(string $file): array + { + return $this->clean($this->wrappedParser->parseFile($file)); + } + + public function parseString(string $sourceCode): array + { + return $this->clean($this->wrappedParser->parseString($sourceCode)); + } + + /** + * @param Stmt[] $ast + * @return Stmt[] + */ + private function clean(array $ast): array + { + /** @var Stmt[] */ + return $this->traverser->traverse($ast); + } + +} diff --git a/src/Parser/CleaningVisitor.php b/src/Parser/CleaningVisitor.php new file mode 100644 index 0000000000..80f5b2f594 --- /dev/null +++ b/src/Parser/CleaningVisitor.php @@ -0,0 +1,104 @@ +nodeFinder = new NodeFinder(); + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Stmt\Function_) { + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); + return $node; + } + + if ($node instanceof Node\Stmt\ClassMethod && $node->stmts !== null) { + $node->stmts = $this->keepVariadicsAndYields($node->stmts, null); + return $node; + } + + if ($node instanceof Node\Expr\Closure) { + $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; + } + + /** + * @param Node\Stmt[] $stmts + * @return Node\Stmt[] + */ + private function keepVariadicsAndYields(array $stmts, ?string $hookedPropertyName): array + { + $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; + } + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + 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 + || $result instanceof Node\Expr\Closure + || $result instanceof Node\Expr\ArrowFunction + || $result instanceof Node\Expr\PropertyFetch + ) { + $newStmts[] = new Node\Stmt\Expression($result); + continue; + } + if (!$result instanceof Node\Expr\FuncCall) { + continue; + } + + $newStmts[] = new Node\Stmt\Expression(new Node\Expr\FuncCall(new Node\Name\FullyQualified('func_get_args'))); + } + + return $newStmts; + } + +} diff --git a/src/Parser/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/DirectParser.php b/src/Parser/DirectParser.php deleted file mode 100644 index c6cb2d1778..0000000000 --- a/src/Parser/DirectParser.php +++ /dev/null @@ -1,50 +0,0 @@ -parser = $parser; - $this->traverser = $traverser; - } - - /** - * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] - */ - public function parseFile(string $file): array - { - return $this->parseString(FileReader::read($file)); - } - - /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] - */ - public function parseString(string $sourceCode): array - { - $errorHandler = new Collecting(); - $nodes = $this->parser->parse($sourceCode, $errorHandler); - if ($errorHandler->hasErrors()) { - throw new \PHPStan\Parser\ParserErrorsException($errorHandler->getErrors()); - } - if ($nodes === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - /** @var array<\PhpParser\Node\Stmt> */ - return $this->traverser->traverse($nodes); - } - -} diff --git a/src/Parser/FunctionCallStatementFinder.php b/src/Parser/FunctionCallStatementFinder.php deleted file mode 100644 index 7336d986d8..0000000000 --- a/src/Parser/FunctionCallStatementFinder.php +++ /dev/null @@ -1,45 +0,0 @@ -findFunctionCallInStatements($functionNames, $statement); - if ($result !== null) { - return $result; - } - } - - if (!($statement instanceof \PhpParser\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 new file mode 100644 index 0000000000..e02bc5ed2c --- /dev/null +++ b/src/Parser/LexerFactory.php @@ -0,0 +1,30 @@ +phpVersion->getVersionId() === PHP_VERSION_ID) { + return new Lexer(); + } + + return new Lexer\Emulative(\PhpParser\PhpVersion::fromString($this->phpVersion->getVersionString())); + } + + 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/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/Parser.php b/src/Parser/Parser.php index 4cea58f895..187d6875c6 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -2,18 +2,22 @@ namespace PHPStan\Parser; +use PhpParser\Node; + +/** @api */ interface Parser { /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] + * @throws ParserErrorsException */ public function parseFile(string $file): array; /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] + * @throws ParserErrorsException */ public function parseString(string $sourceCode): array; diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index 2ab34f8039..d68d18220a 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -2,31 +2,53 @@ namespace PHPStan\Parser; +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 \PhpParser\Error[] */ - private array $errors; + /** @var mixed[] */ + private array $attributes; /** - * @param \PhpParser\Error[] $errors + * @param Error[] $errors */ - public function __construct(array $errors) + public function __construct( + private array $errors, + private ?string $parsedFile, + ) { - parent::__construct(implode(', ', array_map(static function (Error $error): string { - return $error->getMessage(); - }, $errors))); - $this->errors = $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 = []; + } } /** - * @return \PhpParser\Error[] + * @return Error[] */ public function getErrors(): array { return $this->errors; } + 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 new file mode 100644 index 0000000000..83fae0a891 --- /dev/null +++ b/src/Parser/PathRoutingParser.php @@ -0,0 +1,80 @@ + bool(true) */ + private array $analysedFiles = []; + + public function __construct( + private FileHelper $fileHelper, + private Parser $currentPhpVersionRichParser, + private Parser $currentPhpVersionSimpleParser, + private Parser $php8Parser, + ) + { + } + + /** + * @param string[] $files + */ + public function setAnalysedFiles(array $files): void + { + $this->analysedFiles = array_fill_keys($files, true); + } + + public function parseFile(string $file): array + { + $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); + } + + return $this->currentPhpVersionRichParser->parseFile($file); + } + + public function parseString(string $sourceCode): array + { + return $this->currentPhpVersionSimpleParser->parseString($sourceCode); + } + +} diff --git a/src/Parser/PhpParserDecorator.php b/src/Parser/PhpParserDecorator.php index bc66e5e534..7481574450 100644 --- a/src/Parser/PhpParserDecorator.php +++ b/src/Parser/PhpParserDecorator.php @@ -2,30 +2,39 @@ namespace PHPStan\Parser; +use PhpParser\Error; use PhpParser\ErrorHandler; +use PhpParser\Node; +use PhpParser\Parser; +use PHPStan\ShouldNotHappenException; +use function sprintf; -class PhpParserDecorator implements \PhpParser\Parser +final class PhpParserDecorator implements Parser { - private \PHPStan\Parser\Parser $wrappedParser; - - public function __construct(\PHPStan\Parser\Parser $wrappedParser) + public function __construct(private \PHPStan\Parser\Parser $wrappedParser) { - $this->wrappedParser = $wrappedParser; } /** - * @param string $code - * @param \PhpParser\ErrorHandler|null $errorHandler - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parse(string $code, ?ErrorHandler $errorHandler = null): array { try { return $this->wrappedParser->parseString($code); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - throw new \PhpParser\Error($e->getMessage()); + } catch (ParserErrorsException $e) { + $message = $e->getMessage(); + if ($e->getParsedFile() !== null) { + $message .= sprintf(' in file %s', $e->getParsedFile()); + } + 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 new file mode 100644 index 0000000000..b57afc5fba --- /dev/null +++ b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php @@ -0,0 +1,98 @@ +elseifs) > 0) { + return null; + } + + if ($node->else === null) { + return null; + } + + $cond = $node->cond; + if ( + !$cond instanceof Node\Expr\BinaryOp\Smaller + && !$cond instanceof Node\Expr\BinaryOp\SmallerOrEqual + && !$cond instanceof Node\Expr\BinaryOp\Greater + && !$cond instanceof Node\Expr\BinaryOp\GreaterOrEqual + && !$cond instanceof Node\Expr\BinaryOp\Equal + && !$cond instanceof Node\Expr\BinaryOp\NotEqual + && !$cond instanceof Node\Expr\BinaryOp\Identical + && !$cond instanceof Node\Expr\BinaryOp\NotIdentical + ) { + return null; + } + + $operator = $cond->getOperatorSigil(); + if ($operator === '===') { + $operator = '=='; + } elseif ($operator === '!==') { + $operator = '!='; + } + + $operands = $this->getOperands($cond->left, $cond->right); + if ($operands === null) { + return null; + } + + $result = version_compare($operands[0], $operands[1], $operator); + if ($result) { + // remove else + $node->cond = new Node\Expr\ConstFetch(new Node\Name('true')); + $node->else = null; + + return $node; + } + + // remove if + $node->cond = new Node\Expr\ConstFetch(new Node\Name('false')); + $node->stmts = []; + + return $node; + } + + /** + * @return array{string, string}|null + */ + private function getOperands(Node\Expr $left, Node\Expr $right): ?array + { + if ( + $left instanceof Node\Scalar\Int_ + && $right instanceof Node\Expr\ConstFetch + && $right->name->toString() === 'PHP_VERSION_ID' + ) { + return [(new PhpVersion($left->value))->getVersionString(), $this->phpVersionString]; + } + + if ( + $right instanceof Node\Scalar\Int_ + && $left instanceof Node\Expr\ConstFetch + && $left->name->toString() === 'PHP_VERSION_ID' + ) { + return [$this->phpVersionString, (new PhpVersion($right->value))->getVersionString()]; + } + + return null; + } + +} diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php new file mode 100644 index 0000000000..a50ec45dbe --- /dev/null +++ b/src/Parser/RichParser.php @@ -0,0 +1,346 @@ +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); + + $tokens = $this->parser->getTokens(); + if ($errorHandler->hasErrors()) { + throw new ParserErrorsException($errorHandler->getErrors(), null); + } + if ($nodes === null) { + throw new ShouldNotHappenException(); + } + + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($this->nameResolver); + + $traitCollectingVisitor = new TraitCollectingVisitor(); + $nodeTraverser->addVisitor($traitCollectingVisitor); + + foreach ($this->container->getServicesByTag(self::VISITOR_SERVICE_TAG) as $visitor) { + $nodeTraverser->addVisitor($visitor); + } + + /** @var array */ + $nodes = $nodeTraverser->traverse($nodes); + ['lines' => $linesToIgnore, 'errors' => $ignoreParseErrors] = $this->getLinesToIgnore($tokens); + if (isset($nodes[0])) { + $nodes[0]->setAttribute('linesToIgnore', $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 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) { + $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; + } + + $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; + } + + $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->text, "\n"); + $pendingToken = [$text, $ignorePos, $token->line + $ignoreLine, $line]; + } + + if ($pendingToken !== null) { + [$pendingText, $pendingIgnorePos, $tokenLine, $pendingLine] = $pendingToken; + + try { + foreach ($this->parseIdentifiers($pendingText, $pendingIgnorePos) as $identifier) { + $lines[$pendingLine][] = $identifier; + } + } catch (IgnoreParseException $e) { + $errors[] = [$tokenLine + $e->getPhpDocLine(), $e->getMessage()]; + } + } + + $processedErrors = []; + foreach ($errors as [$line, $message]) { + $processedErrors[$line][] = $message; + } + + return [ + 'lines' => $lines, + 'errors' => $processedErrors, + ]; + } + + /** + * @return array + */ + private function getLinesToIgnoreForTokenByIgnoreComment( + string $tokenText, + int $tokenLine, + string $ignoreComment, + bool $ignoreNextLine, + ): 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 new file mode 100644 index 0000000000..8fbd112742 --- /dev/null +++ b/src/Parser/SimpleParser.php @@ -0,0 +1,60 @@ +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); + $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 e844809c22..5215a30606 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -2,14 +2,75 @@ namespace PHPStan\Php; -class PhpVersion +use function floor; + +/** + * @api + */ +final class PhpVersion { - 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 __construct(int $versionId) + public function getMajorVersionId(): int { - $this->versionId = $versionId; + 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 = $this->getMajorVersionId(); + $second = $this->getMinorVersionId(); + $third = $this->getPatchVersionId(); + + return $first . '.' . $second . ($third !== 0 ? '.' . $third : ''); } public function supportsNullCoalesceAssign(): bool @@ -27,4 +88,321 @@ public function supportsReturnCovariance(): bool return $this->versionId >= 70400; } + public function supportsNoncapturingCatches(): bool + { + return $this->versionId >= 80000; + } + + public function supportsNativeUnionTypes(): bool + { + return $this->versionId >= 80000; + } + + 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; + } + + public function supportsThrowExpression(): bool + { + return $this->versionId >= 80000; + } + + public function supportsClassConstantOnExpression(): bool + { + return $this->versionId >= 80000; + } + + public function supportsLegacyConstructor(): bool + { + return $this->versionId < 80000; + } + + public function supportsPromotedProperties(): bool + { + return $this->versionId >= 80000; + } + + public function supportsParameterTypeWidening(): bool + { + return $this->versionId >= 70200; + } + + public function supportsUnsetCast(): bool + { + return $this->versionId < 80000; + } + + public function supportsNamedArguments(): bool + { + return $this->versionId >= 80000; + } + + public function throwsTypeErrorForInternalFunctions(): bool + { + return $this->versionId >= 80000; + } + + public function throwsValueErrorForInternalFunctions(): bool + { + return $this->versionId >= 80000; + } + + public function supportsHhPrintfSpecifier(): bool + { + return $this->versionId >= 80000; + } + + public function isEmptyStringValidAliasForNoneInMbSubstituteCharacter(): bool + { + return $this->versionId < 80000; + } + + public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): bool + { + return $this->versionId >= 70200; + } + + public function isNumericStringValidArgInMbSubstituteCharacter(): bool + { + return $this->versionId < 80000; + } + + public function isNullValidArgInMbSubstituteCharacter(): bool + { + return $this->versionId >= 80000; + } + + public function isInterfaceConstantImplicitlyFinal(): bool + { + return $this->versionId < 80100; + } + + public function supportsFinalConstants(): bool + { + return $this->versionId >= 80100; + } + + public function supportsReadOnlyProperties(): bool + { + return $this->versionId >= 80100; + } + + public function supportsEnums(): bool + { + return $this->versionId >= 80100; + } + + public function supportsPureIntersectionTypes(): bool + { + return $this->versionId >= 80100; + } + + public function supportsCaseInsensitiveConstantNames(): bool + { + return $this->versionId < 80000; + } + + public function hasStricterRoundFunctions(): bool + { + return $this->versionId >= 80000; + } + + public function hasTentativeReturnTypes(): bool + { + return $this->versionId >= 80100; + } + + public function supportsFirstClassCallables(): bool + { + return $this->versionId >= 80100; + } + + public function supportsArrayUnpackingWithStringKeys(): bool + { + return $this->versionId >= 80100; + } + + public function throwsOnInvalidMbStringEncoding(): bool + { + return $this->versionId >= 80000; + } + + 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 13ac90b56c..bd1bfcabf5 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -2,26 +2,43 @@ namespace PHPStan\Php; +use function explode; +use function max; +use function min; use const PHP_VERSION_ID; -class PhpVersionFactory +final class PhpVersionFactory { - private ?int $versionId; + 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(?int $versionId) + public function __construct( + private ?int $versionId, + private ?string $composerPhpVersion, + ) { - $this->versionId = $versionId; } public function create(): PhpVersion { $versionId = $this->versionId; - if ($versionId === 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, 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 new file mode 100644 index 0000000000..0190ca0e82 --- /dev/null +++ b/src/Php/PhpVersionFactoryFactory.php @@ -0,0 +1,62 @@ +composerAutoloaderProjectPaths) > 0) { + $composerJsonPath = end($this->composerAutoloaderProjectPaths) . '/composer.json'; + if (is_file($composerJsonPath)) { + try { + $composerJsonContents = FileReader::read($composerJsonPath); + $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); + $platformVersion = $composer['config']['platform']['php'] ?? null; + if (is_string($platformVersion)) { + $composerPhpVersion = $platformVersion; + } + } catch (CouldNotReadFileException | JsonException) { + // pass + } + } + } + + $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 291a376256..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,61 +11,129 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; -use PHPStan\Type\ArrayType; +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): ArrayType + 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 new file mode 100644 index 0000000000..c1c038ca81 --- /dev/null +++ b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php @@ -0,0 +1,17 @@ +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 f6cf2220ff..7c00dfe34d 100644 --- a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php @@ -2,24 +2,23 @@ namespace PHPStan\PhpDoc; -class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider -{ +use PHPStan\DependencyInjection\Container; - private \PHPStan\DependencyInjection\Container $container; +final class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider +{ private ?TypeNodeResolverExtensionRegistry $registry = null; - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): TypeNodeResolverExtensionRegistry { if ($this->registry === null) { - $this->registry = new TypeNodeResolverExtensionRegistry( + $this->registry = new TypeNodeResolverExtensionAwareRegistry( $this->container->getByType(TypeNodeResolver::class), - $this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG) + $this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG), ); } diff --git a/src/PhpDoc/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 @@ +phpDocString = $phpDocString; - $this->nameScope = $nameScope; - } - - public function getPhpDocString(): string - { - return $this->phpDocString; - } - - public function getNameScope(): NameScope - { - return $this->nameScope; - } - - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['phpDocString'], - $properties['nameScope'] - ); - } - -} diff --git a/src/PhpDoc/PhpDocBlock.php b/src/PhpDoc/PhpDocBlock.php index 880e8a92b4..8036cd78ee 100644 --- a/src/PhpDoc/PhpDocBlock.php +++ b/src/PhpDoc/PhpDocBlock.php @@ -2,58 +2,36 @@ namespace PHPStan\PhpDoc; +use PHPStan\PhpDoc\Tag\AssertTagParameter; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Reflection\ResolvedMethodReflection; -use PHPStan\Reflection\ResolvedPropertyReflection; - -class PhpDocBlock +use PHPStan\Type\ConditionalTypeForParameter; +use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; +use function array_key_exists; +use function count; +use function is_bool; +use function strtolower; +use function substr; + +final class PhpDocBlock { - private string $docComment; - - private string $file; - - private ClassReflection $classReflection; - - private ?string $trait; - - private bool $explicit; - - /** @var array */ - private array $parameterNameMapping; - - /** @var array */ - private array $parents; - /** - * @param string $docComment - * @param string $file - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param bool $explicit * @param array $parameterNameMapping * @param array $parents */ private function __construct( - string $docComment, - string $file, - ClassReflection $classReflection, - ?string $trait, - bool $explicit, - array $parameterNameMapping, - array $parents + private string $docComment, + private ?string $file, + private ClassReflection $classReflection, + private ?string $trait, + private bool $explicit, + private array $parameterNameMapping, + private array $parents, ) { - $this->docComment = $docComment; - $this->file = $file; - $this->classReflection = $classReflection; - $this->trait = $trait; - $this->explicit = $explicit; - $this->parameterNameMapping = $parameterNameMapping; - $this->parents = $parents; } public function getDocComment(): string @@ -61,7 +39,7 @@ public function getDocComment(): string return $this->docComment; } - public function getFile(): string + public function getFile(): ?string { return $this->file; } @@ -107,126 +85,169 @@ public function transformArrayKeysWithParameterNameMapping(array $array): array return $newArray; } - /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $propertyName - * @param string $file - * @param bool|null $explicit - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - * @return self - */ + public 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 string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $methodName - * @param string $file - * @param bool|null $explicit - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - * @return self - */ - public static function resolvePhpDocBlockForMethod( + public static function resolvePhpDocBlockForConstant( ?string $docComment, ClassReflection $classReflection, - ?string $trait, - string $methodName, - string $file, + string $constantName, + ?string $file, ?bool $explicit, - array $originalPositionalParameterNames, - array $newPositionalParameterNames ): self { - return self::resolvePhpDocBlockTree( - $docComment, - $classReflection, - $trait, - $methodName, + $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, - 'hasNativeMethod', - 'getNativeMethod', - __FUNCTION__, - $explicit, - $originalPositionalParameterNames, - $newPositionalParameterNames + $classReflection, + null, + $explicit ?? true, + [], + $docBlocksFromParents, ); } /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $name - * @param string $file - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool|null $explicit * @param array $originalPositionalParameterNames * @param array $newPositionalParameterNames - * @return self */ - private static function resolvePhpDocBlockTree( + public static function resolvePhpDocBlockForMethod( ?string $docComment, ClassReflection $classReflection, ?string $trait, - string $name, - string $file, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, + string $methodName, + ?string $file, ?bool $explicit, array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): self { - $docBlocksFromParents = self::resolveParentPhpDocBlocks( - $classReflection, - $name, - $hasMethodName, - $getMethodName, - $resolveMethodName, - $explicit ?? $docComment !== null, - $newPositionalParameterNames - ); + $docBlocksFromParents = []; + foreach (self::getParentReflections($classReflection) as $parentReflection) { + $oneResult = self::resolveMethodPhpDocBlockFromClass( + $parentReflection, + $methodName, + $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, $explicit ?? true, self::remapParameterNames($originalPositionalParameterNames, $newPositionalParameterNames), - $docBlocksFromParents + $docBlocksFromParents, ); } @@ -237,7 +258,7 @@ private static function resolvePhpDocBlockTree( */ private static function remapParameterNames( array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): array { $parameterNameMapping = []; @@ -252,137 +273,132 @@ private static function remapParameterNames( } /** - * @param ClassReflection $classReflection - * @param string $name - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool $explicit - * @param array $positionalParameterNames - * @return array + * @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; } - /** - * @param ClassReflection $classReflection - * @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 !== false) { - $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 \PHPStan\Reflection\ClassReflection $classReflection - * @param string $name - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool $explicit * @param array $positionalParameterNames - * @return self|null */ - private static function resolvePhpDocBlockFromClass( + private static function resolveMethodPhpDocBlockFromClass( ClassReflection $classReflection, string $name, - string $hasMethodName, - string $getMethodName, - string $resolveMethodName, bool $explicit, - array $positionalParameterNames + array $positionalParameterNames, ): ?self { - if ($classReflection->getFileNameWithPhpDocs() !== null && $classReflection->$hasMethodName($name)) { - /** @var \PHPStan\Reflection\PropertyReflection|\PHPStan\Reflection\MethodReflection $parentReflection */ - $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, - $classReflection->getFileNameWithPhpDocs(), + $classReflection->getFileName(), $explicit, $positionalParameterNames, - $positionalMethodParameterNames + $positionalMethodParameterNames, ); } diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 3cac015b1e..5b6aaacc3b 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -2,28 +2,29 @@ namespace PHPStan\PhpDoc; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\FileTypeMapper; +use function array_map; +use function strtolower; -class PhpDocInheritanceResolver +final class PhpDocInheritanceResolver { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - public function __construct( - FileTypeMapper $fileTypeMapper + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, ) { - $this->fileTypeMapper = $fileTypeMapper; } public function resolvePhpDocForProperty( ?string $docComment, ClassReflection $classReflection, - string $classReflectionFileName, + ?string $classReflectionFileName, ?string $declaringTraitName, - string $propertyName - ): ?ResolvedPhpDocBlock + string $propertyName, + ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty( $docComment, @@ -32,29 +33,39 @@ public function resolvePhpDocForProperty( $propertyName, $classReflectionFileName, null, - [], - [] ); - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null); + return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null, $propertyName, null); + } + + public function resolvePhpDocForConstant( + ?string $docComment, + ClassReflection $classReflection, + ?string $classReflectionFileName, + string $constantName, + ): ResolvedPhpDocBlock + { + $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForConstant( + $docComment, + $classReflection, + $constantName, + $classReflectionFileName, + null, + ); + + return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, null, null, null, $constantName); } /** - * @param string|null $docComment - * @param string $fileName - * @param ClassReflection $classReflection - * @param string|null $declaringTraitName - * @param string $methodName * @param array $positionalParameterNames - * @return ResolvedPhpDocBlock */ public function resolvePhpDocForMethod( ?string $docComment, - string $fileName, + ?string $fileName, ClassReflection $classReflection, ?string $declaringTraitName, string $methodName, - array $positionalParameterNames + array $positionalParameterNames, ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod( @@ -65,40 +76,80 @@ public function resolvePhpDocForMethod( $fileName, null, $positionalParameterNames, - $positionalParameterNames + $positionalParameterNames, ); - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $phpDocBlock->getTrait(), $methodName); + return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $phpDocBlock->getTrait(), $methodName, null, null); } - private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName): ResolvedPhpDocBlock + private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName, ?string $propertyName, ?string $constantName): ResolvedPhpDocBlock { $parents = []; $parentPhpDocBlocks = []; foreach ($phpDocBlock->getParents() as $parentPhpDocBlock) { + if ( + $functionName !== null + && strtolower($functionName) === '__construct' + && $parentPhpDocBlock->getClassReflection()->isBuiltin() + ) { + continue; + } $parents[] = $this->docBlockTreeToResolvedDocBlock( $parentPhpDocBlock, $parentPhpDocBlock->getTrait(), - $functionName + $functionName, + $propertyName, + $constantName, ); $parentPhpDocBlocks[] = $parentPhpDocBlock; } - $oneResolvedDockBlock = $this->docBlockToResolvedDocBlock($phpDocBlock, $traitName, $functionName); + $oneResolvedDockBlock = $this->docBlockToResolvedDocBlock($phpDocBlock, $traitName, $functionName, $propertyName, $constantName); return $oneResolvedDockBlock->merge($parents, $parentPhpDocBlocks); } - private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName): ResolvedPhpDocBlock + private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName, ?string $propertyName, ?string $constantName): ResolvedPhpDocBlock { $classReflection = $phpDocBlock->getClassReflection(); + if ($functionName !== null && $classReflection->getNativeReflection()->hasMethod($functionName)) { + $methodReflection = $classReflection->getNativeReflection()->getMethod($functionName); + $stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); + if ($stub !== null) { + return $stub; + } + } + + if ($propertyName !== null && $classReflection->getNativeReflection()->hasProperty($propertyName)) { + $stub = $this->stubPhpDocProvider->findPropertyPhpDoc($classReflection->getName(), $propertyName); + + if ($stub === null) { + $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; + } + } + + if ($constantName !== null && $classReflection->getNativeReflection()->hasConstant($constantName)) { + $stub = $this->stubPhpDocProvider->findClassConstantPhpDoc($classReflection->getName(), $constantName); + if ($stub !== null) { + return $stub; + } + } return $this->fileTypeMapper->getResolvedPhpDoc( $phpDocBlock->getFile(), $classReflection->getName(), $traitName, $functionName, - $phpDocBlock->getDocComment() + $phpDocBlock->getDocComment(), ); } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 31a8e79639..1c31ee55d0 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -3,54 +3,72 @@ 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; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; use PHPStan\Reflection\PassedByReference; -use PHPStan\Type\ErrorType; +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\NeverType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; - -class PhpDocNodeResolver +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_reverse; +use function count; +use function in_array; +use function method_exists; +use function str_starts_with; +use function substr; + +final class PhpDocNodeResolver { - private TypeNodeResolver $typeNodeResolver; - - private ConstExprNodeResolver $constExprNodeResolver; - - public function __construct(TypeNodeResolver $typeNodeResolver, ConstExprNodeResolver $constExprNodeResolver) + public function __construct( + private TypeNodeResolver $typeNodeResolver, + private ConstExprNodeResolver $constExprNodeResolver, + private UnresolvableTypeHelper $unresolvableTypeHelper, + ) { - $this->typeNodeResolver = $typeNodeResolver; - $this->constExprNodeResolver = $constExprNodeResolver; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array<(string|int), VarTag> */ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { - foreach (['@phpstan-var', '@psalm-var', '@var'] as $tagName) { - $resolved = []; + $resolved = []; + $resolvedByTag = []; + foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) { + $tagResolved = []; foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); if ($this->shouldSkipType($tagName, $type)) { @@ -60,117 +78,163 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar $variableName = substr($tagValue->variableName, 1); $resolved[$variableName] = new VarTag($type); } else { - $resolved[] = new VarTag($type); + $varTag = new VarTag($type); + $tagResolved[] = $varTag; } } - if (count($resolved) > 0) { - return $resolved; + if (count($tagResolved) === 0) { + continue; } + + $resolvedByTag[] = $tagResolved; } - return []; + if (count($resolvedByTag) > 0) { + return array_reverse($resolvedByTag)[0]; + } + + return $resolved; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; - foreach ($phpDocNode->getPropertyTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property', '@phpstan-property'] as $tagName) { + foreach ($phpDocNode->getPropertyTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - true, - true - ); + $resolved[$propertyName] = new PropertyTag( + $propertyType, + $propertyType, + ); + } } - foreach ($phpDocNode->getPropertyReadTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property-read', '@phpstan-property-read'] as $tagName) { + foreach ($phpDocNode->getPropertyReadTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - true, - false - ); + $writableType = null; + if (array_key_exists($propertyName, $resolved)) { + $writableType = $resolved[$propertyName]->getWritableType(); + } + + $resolved[$propertyName] = new PropertyTag( + $propertyType, + $writableType, + ); + } } - foreach ($phpDocNode->getPropertyWriteTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property-write', '@phpstan-property-write'] as $tagName) { + foreach ($phpDocNode->getPropertyWriteTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - false, - true - ); + $readableType = null; + if (array_key_exists($propertyName, $resolved)) { + $readableType = $resolved[$propertyName]->getReadableType(); + } + + $resolved[$propertyName] = new PropertyTag( + $readableType, + $propertyType, + ); + } } return $resolved; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; - - foreach ($phpDocNode->getMethodTagValues() as $tagValue) { - $parameters = []; - foreach ($tagValue->parameters as $parameterNode) { - $parameterName = substr($parameterNode->parameterName, 1); - $type = $parameterNode->type !== null ? $this->typeNodeResolver->resolve($parameterNode->type, $nameScope) : new MixedType(); - if ($parameterNode->defaultValue instanceof ConstExprNullNode) { - $type = TypeCombinator::addNull($type); + $originalNameScope = $nameScope; + + 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); } - $defaultValue = null; - if ($parameterNode->defaultValue !== null) { - $defaultValue = $this->constExprNodeResolver->resolve($parameterNode->defaultValue); + + $parameters = []; + foreach ($tagValue->parameters as $parameterNode) { + $parameterName = substr($parameterNode->parameterName, 1); + $type = $parameterNode->type !== null + ? $this->typeNodeResolver->resolve($parameterNode->type, $nameScope) + : new MixedType(); + if ($parameterNode->defaultValue instanceof ConstExprNullNode) { + $type = TypeCombinator::addNull($type); + } + $defaultValue = null; + if ($parameterNode->defaultValue !== null) { + $defaultValue = $this->constExprNodeResolver->resolve($parameterNode->defaultValue, $nameScope); + } + + $parameters[$parameterName] = new MethodTagParameter( + $type, + $parameterNode->isReference + ? PassedByReference::createCreatesNewVariable() + : PassedByReference::createNo(), + $parameterNode->isVariadic || $parameterNode->defaultValue !== null, + $parameterNode->isVariadic, + $defaultValue, + ); } - $parameters[$parameterName] = new MethodTagParameter( - $type, - $parameterNode->isReference - ? PassedByReference::createCreatesNewVariable() - : PassedByReference::createNo(), - $parameterNode->isVariadic || $parameterNode->defaultValue !== null, - $parameterNode->isVariadic, - $defaultValue + $resolved[$tagValue->methodName] = new MethodTag( + $tagValue->returnType !== null + ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) + : new MixedType(), + $tagValue->isStatic, + $parameters, + $templateTags, ); } - - $resolved[$tagValue->methodName] = new MethodTag( - $tagValue->returnType !== null ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) : new MixedType(), - $tagValue->isStatic, - $parameters - ); } return $resolved; } /** - * @return array + * @return array */ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $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[$tagValue->type->type->name] = new ExtendsTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -179,7 +243,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) } /** - * @return array + * @return array */ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -187,8 +251,8 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco foreach (['@implements', '@template-implements', '@phpstan-implements'] as $tagName) { foreach ($phpDocNode->getImplementsTagValues($tagName) as $tagValue) { - $resolved[$tagValue->type->type->name] = new ImplementsTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ImplementsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -197,7 +261,7 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco } /** - * @return array + * @return array */ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -205,8 +269,8 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a foreach (['@use', '@template-use', '@phpstan-use'] as $tagName) { foreach ($phpDocNode->getUsesTagValues($tagName) as $tagValue) { - $resolved[$tagValue->type->type->name] = new UsesTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new UsesTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -215,9 +279,7 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -226,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) { @@ -237,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 = ''; @@ -260,10 +327,13 @@ 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) : new MixedType(), - $variance + $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; } @@ -272,15 +342,13 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $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); @@ -290,7 +358,35 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): $resolved[$parameterName] = new ParamTag( $parameterType, - $tagValue->isVariadic + $tagValue->isVariadic, + ); + } + } + + 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, ); } } @@ -298,11 +394,54 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } - public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\ReturnTag + /** + * @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)) { @@ -315,34 +454,177 @@ public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } - public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\ThrowsTag + public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): ?ThrowsTag { - $types = array_map(function (ThrowsTagValueNode $throwsTagValue) use ($nameScope): Type { - return $this->typeNodeResolver->resolve($throwsTagValue->type, $nameScope); - }, $phpDocNode->getThrowsTagValues()); + foreach (['@phpstan-throws', '@throws'] as $tagName) { + $types = []; - if (count($types) === 0) { - return null; + foreach ($phpDocNode->getThrowsTagValues($tagName) as $tagValue) { + $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + if ($this->shouldSkipType($tagName, $type)) { + continue; + } + + $types[] = $type; + } + + if (count($types) > 0) { + return new ThrowsTag(TypeCombinator::union(...$types)); + } } - return new ThrowsTag(TypeCombinator::union(...$types)); + return null; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope * @return array */ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { - return array_map(function (MixinTagValueNode $mixinTagValueNode) use ($nameScope): MixinTag { - return new MixinTag( - $this->typeNodeResolver->resolve($mixinTagValueNode->type, $nameScope) + return array_map(fn (MixinTagValueNode $mixinTagValueNode): MixinTag => new MixinTag( + $this->typeNodeResolver->resolve($mixinTagValueNode->type, $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 + */ + public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@phan-type', '@psalm-type', '@phpstan-type'] as $tagName) { + foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { + $alias = $typeAliasTagValue->alias; + $typeNode = $typeAliasTagValue->type; + $resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) { + foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { + $importedAlias = $typeAliasImportTagValue->importedAlias; + $importedFrom = $nameScope->resolveStringName($typeAliasImportTagValue->importedFrom->name); + $importedAs = $typeAliasImportTagValue->importedAs; + $resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs); + } + } + + 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), ); - }, $phpDocNode->getMixinTagValues()); + + 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): ?\PHPStan\PhpDoc\Tag\DeprecatedTag + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { $description = (string) $deprecatedTagValue; @@ -359,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'); @@ -373,17 +662,92 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool return count($finalTags) > 0; } + public function resolveIsPure(PhpDocNode $phpDocNode): bool + { + foreach ($phpDocNode->getTags() as $phpDocTagNode) { + if (in_array($phpDocTagNode->name, ['@pure', '@phan-pure', '@phan-side-effect-free', '@psalm-pure', '@phpstan-pure'], true)) { + return true; + } + } + + return false; + } + + public function resolveIsImpure(PhpDocNode $phpDocNode): bool + { + foreach ($phpDocNode->getTags() as $phpDocTagNode) { + if (in_array($phpDocTagNode->name, ['@impure', '@phpstan-impure'], true)) { + return true; + } + } + + 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; } - if ($type instanceof ErrorType) { - return true; + 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 $type instanceof NeverType && !$type->isExplicit(); + return false; } } diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index 736210037f..7c8129a3cc 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -7,24 +7,18 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; -class PhpDocStringResolver +final class PhpDocStringResolver { - private Lexer $phpDocLexer; - - private PhpDocParser $phpDocParser; - - public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) { - $this->phpDocLexer = $phpDocLexer; - $this->phpDocParser = $phpDocParser; } public function resolve(string $phpDocString): PhpDocNode { $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); $phpDocNode = $this->phpDocParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); + $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 0a6af89e6f..6cd991341b 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -3,106 +3,193 @@ 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; +use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\TypedTag; +use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; - -class ResolvedPhpDocBlock +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 + */ +final class ResolvedPhpDocBlock { + public const EMPTY_DOC_STRING = '/** */'; + private PhpDocNode $phpDocNode; + /** @var PhpDocNode[] */ + private array $phpDocNodes; + private string $phpDocString; private ?string $filename; - private NameScope $nameScope; + private ?NameScope $nameScope = null; private TemplateTypeMap $templateTypeMap; - /** @var array */ + /** @var array */ private array $templateTags; - private \PHPStan\PhpDoc\PhpDocNodeResolver $phpDocNodeResolver; + private PhpDocNodeResolver $phpDocNodeResolver; + + private ReflectionProvider $reflectionProvider; + + /** @var array<(string|int), VarTag>|false */ + private array|false $varTags = false; + + /** @var array|false */ + private array|false $methodTags = false; - /** @var array|false */ - private $varTags = false; + /** @var array|false */ + private array|false $propertyTags = false; - /** @var array|false */ - private $methodTags = false; + /** @var array|false */ + private array|false $extendsTags = false; - /** @var array|false */ - private $propertyTags = false; + /** @var array|false */ + private array|false $implementsTags = false; - /** @var array|false */ - private $extendsTags = false; + /** @var array|false */ + private array|false $usesTags = false; - /** @var array|false */ - private $implementsTags = false; + /** @var array|false */ + private array|false $paramTags = false; - /** @var array|false */ - private $usesTags = false; + /** @var array|false */ + private array|false $paramOutTags = false; - /** @var array|false */ - private $paramTags = false; + /** @var array|false */ + private array|false $paramsImmediatelyInvokedCallable = false; - /** @var \PHPStan\PhpDoc\Tag\ReturnTag|false|null */ - private $returnTag = false; + /** @var array|false */ + private array|false $paramClosureThisTags = false; - /** @var \PHPStan\PhpDoc\Tag\ThrowsTag|false|null */ - private $throwsTag = false; + private ReturnTag|false|null $returnTag = false; + + private ThrowsTag|false|null $throwsTag = false; /** @var array|false */ - private $mixinTags = 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 \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */ - private $deprecatedTag = 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; + /** @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() { } /** - * @param \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode $phpDocNode - * @param string $phpDocString - * @param string $filename - * @param \PHPStan\Analyser\NameScope $nameScope - * @param \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap - * @param \PHPStan\PhpDoc\Tag\TemplateTag[] $templateTags - * @param \PHPStan\PhpDoc\PhpDocNodeResolver $phpDocNodeResolver - * @return self + * @param TemplateTag[] $templateTags */ public static function create( PhpDocNode $phpDocNode, string $phpDocString, - string $filename, + ?string $filename, NameScope $nameScope, TemplateTypeMap $templateTypeMap, array $templateTags, - PhpDocNodeResolver $phpDocNodeResolver + 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]; $self->phpDocString = $phpDocString; $self->filename = $filename; $self->nameScope = $nameScope; $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; } @@ -111,7 +198,8 @@ 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(); $self->templateTags = []; @@ -122,13 +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; } @@ -136,15 +240,28 @@ public static function createEmpty(): self /** * @param array $parents * @param array $parentPhpDocBlocks - * @return 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; @@ -157,40 +274,119 @@ 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->deprecatedTag = $this->getDeprecatedTag(); + $result->requireExtendsTags = $this->getRequireExtendsTags(); + $result->requireImplementsTags = $this->getRequireImplementsTags(); + $result->typeAliasTags = $this->getTypeAliasTags(); + $result->typeAliasImportTags = $this->getTypeAliasImportTags(); + $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); + $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; + $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); + $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; } /** * @param array $parameterNameMapping - * @return self */ public function changeParameterNamesByMapping(array $parameterNameMapping): self { - $paramTags = $this->getParamTags(); + if (count($this->phpDocNodes) === 0) { + return $this; + } + + $mapParameterCb = static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $parameterNameMapping)) { + $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]); + } + } + + return $traverse($type); + }; $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(); $self->phpDocNode = $this->phpDocNode; + $self->phpDocNodes = $this->phpDocNodes; $self->phpDocString = $this->phpDocString; $self->filename = $this->filename; $self->nameScope = $this->nameScope; $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; @@ -198,70 +394,105 @@ 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; return $self; } + public function hasPhpDocString(): bool + { + return $this->phpDocString !== self::EMPTY_DOC_STRING; + } + public function getPhpDocString(): string { return $this->phpDocString; } + /** + * @return PhpDocNode[] + */ + public function getPhpDocNodes(): array + { + return $this->phpDocNodes; + } + public function getFilename(): ?string { return $this->filename; } + private function getNameScope(): NameScope + { + return $this->nameScope; + } + + public function getNullableNameScope(): ?NameScope + { + return $this->nameScope; + } + /** - * @return array + * @return array<(string|int), VarTag> */ public function getVarTags(): array { if ($this->varTags === false) { $this->varTags = $this->phpDocNodeResolver->resolveVarTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->varTags; } /** - * @return array + * @return array */ public function getMethodTags(): array { if ($this->methodTags === false) { $this->methodTags = $this->phpDocNodeResolver->resolveMethodTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->methodTags; } /** - * @return array + * @return array */ public function getPropertyTags(): array { if ($this->propertyTags === false) { $this->propertyTags = $this->phpDocNodeResolver->resolvePropertyTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->propertyTags; } /** - * @return array + * @return array */ public function getTemplateTags(): array { @@ -269,78 +500,119 @@ public function getTemplateTags(): array } /** - * @return array + * @return array */ public function getExtendsTags(): array { if ($this->extendsTags === false) { $this->extendsTags = $this->phpDocNodeResolver->resolveExtendsTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->extendsTags; } /** - * @return array + * @return array */ public function getImplementsTags(): array { if ($this->implementsTags === false) { $this->implementsTags = $this->phpDocNodeResolver->resolveImplementsTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->implementsTags; } /** - * @return array + * @return array */ public function getUsesTags(): array { if ($this->usesTags === false) { $this->usesTags = $this->phpDocNodeResolver->resolveUsesTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->usesTags; } /** - * @return array + * @return array */ public function getParamTags(): array { if ($this->paramTags === false) { $this->paramTags = $this->phpDocNodeResolver->resolveParamTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->paramTags; } - public function getReturnTag(): ?\PHPStan\PhpDoc\Tag\ReturnTag + /** + * @return array + */ + public function getParamOutTags(): array { - if ($this->returnTag === false) { + 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)) { $this->returnTag = $this->phpDocNodeResolver->resolveReturnTag( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->returnTag; } - public function getThrowsTag(): ?\PHPStan\PhpDoc\Tag\ThrowsTag + public function getThrowsTag(): ?ThrowsTag { - if ($this->throwsTag === false) { + if (is_bool($this->throwsTag)) { $this->throwsTag = $this->phpDocNodeResolver->resolveThrowsTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->throwsTag; @@ -354,19 +626,106 @@ public function getMixinTags(): array if ($this->mixinTags === false) { $this->mixinTags = $this->phpDocNodeResolver->resolveMixinTags( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->mixinTags; } - public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag + /** + * @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 + */ + public function getTypeAliasTags(): array + { + if ($this->typeAliasTags === false) { + $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->typeAliasTags; + } + + /** + * @return array + */ + public function getTypeAliasImportTags(): array + { + if ($this->typeAliasImportTags === false) { + $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + 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->deprecatedTag === false) { + 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)) { $this->deprecatedTag = $this->phpDocNodeResolver->resolveDeprecatedTag( $this->phpDocNode, - $this->nameScope + $this->getNameScope(), ); } return $this->deprecatedTag; @@ -376,17 +735,30 @@ public function isDeprecated(): bool { if ($this->isDeprecated === null) { $this->isDeprecated = $this->phpDocNodeResolver->resolveIsDeprecated( - $this->phpDocNode + $this->phpDocNode, ); } 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) { $this->isInternal = $this->phpDocNodeResolver->resolveIsInternal( - $this->phpDocNode + $this->phpDocNode, ); } return $this->isInternal; @@ -396,17 +768,93 @@ public function isFinal(): bool { if ($this->isFinal === null) { $this->isFinal = $this->phpDocNodeResolver->resolveIsFinal( - $this->phpDocNode + $this->phpDocNode, ); } 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; } + public function isPure(): ?bool + { + if ($this->isPure === 'notLoaded') { + $pure = $this->phpDocNodeResolver->resolveIsPure( + $this->phpDocNode, + ); + if ($pure) { + $this->isPure = true; + return $this->isPure; + } + + $impure = $this->phpDocNodeResolver->resolveIsImpure( + $this->phpDocNode, + ); + if ($impure) { + $this->isPure = false; + return $this->isPure; + } + + $this->isPure = null; + } + + 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 @@ -433,14 +881,12 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare } /** - * @param ResolvedPhpDocBlock $parent - * @param PhpDocBlock $phpDocBlock * @return array|null */ private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array { foreach ($parent->getVarTags() as $key => $parentVarTag) { - return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock)]; + return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock, TemplateTypeVariance::createInvariant())]; } return null; @@ -463,8 +909,6 @@ private static function mergeParamTags(array $paramTags, array $parents, array $ /** * @param array $paramTags - * @param ResolvedPhpDocBlock $parent - * @param PhpDocBlock $phpDocBlock * @return array */ private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array @@ -476,26 +920,29 @@ 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; } /** - * @param ReturnTag|null $returnTag * @param array $parents * @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; } @@ -506,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) { @@ -515,13 +962,111 @@ 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 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 && !$parent->isNotDeprecated()) { + continue; + } + return $result; + } + + return null; } /** @@ -545,16 +1090,154 @@ private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): } /** - * @template T of \PHPStan\PhpDoc\Tag\TypedTag + * @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 - * @param PhpDocBlock $phpDocBlock * @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()->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 new file mode 100644 index 0000000000..91267fa583 --- /dev/null +++ b/src/PhpDoc/StubFilesExtension.php @@ -0,0 +1,29 @@ + */ private array $classMap = []; /** @var array> */ private array $propertyMap = []; + /** @var array> */ + private array $constantMap = []; + /** @var array> */ private array $methodMap = []; @@ -46,25 +45,24 @@ class StubPhpDocProvider /** @var array> */ private array $knownPropertiesDocComments = []; + /** @var array> */ + private array $knownConstantsDocComments = []; + /** @var array> */ private array $knownMethodsDocComments = []; /** @var array>> */ private array $knownMethodsParameterNames = []; - /** - * @param \PHPStan\Parser\Parser $parser - * @param string[] $stubFiles - */ + /** @var array> */ + private array $knownFunctionParameterNames = []; + public function __construct( - Parser $parser, - FileTypeMapper $fileTypeMapper, - array $stubFiles + private Parser $parser, + private FileTypeMapper $fileTypeMapper, + private StubFilesProvider $stubFilesProvider, ) { - $this->parser = $parser; - $this->fileTypeMapper = $fileTypeMapper; - $this->stubFiles = $stubFiles; } public function findClassPhpDoc(string $className): ?ResolvedPhpDocBlock @@ -84,7 +82,7 @@ public function findClassPhpDoc(string $className): ?ResolvedPhpDocBlock $className, null, null, - $docComment + $docComment, ); return $this->classMap[$className]; @@ -110,7 +108,7 @@ public function findPropertyPhpDoc(string $className, string $propertyName): ?Re $className, null, null, - $docComment + $docComment, ); return $this->propertyMap[$className][$propertyName]; @@ -119,13 +117,41 @@ public function findPropertyPhpDoc(string $className, string $propertyName): ?Re return null; } + public function findClassConstantPhpDoc(string $className, string $constantName): ?ResolvedPhpDocBlock + { + if (!$this->isKnownClass($className)) { + return null; + } + + if (array_key_exists($constantName, $this->constantMap[$className])) { + return $this->constantMap[$className][$constantName]; + } + + if (array_key_exists($constantName, $this->knownConstantsDocComments[$className])) { + [$file, $docComment] = $this->knownConstantsDocComments[$className][$constantName]; + $this->constantMap[$className][$constantName] = $this->fileTypeMapper->getResolvedPhpDoc( + $file, + $className, + null, + null, + $docComment, + ); + + return $this->constantMap[$className][$constantName]; + } + + return null; + } + /** - * @param string $className - * @param string $methodName * @param array $positionalParameterNames - * @return \PHPStan\PhpDoc\ResolvedPhpDocBlock|null */ - public function findMethodPhpDoc(string $className, string $methodName, array $positionalParameterNames): ?ResolvedPhpDocBlock + public function findMethodPhpDoc( + string $className, + string $implementingClassName, + string $methodName, + array $positionalParameterNames, + ): ?ResolvedPhpDocBlock { if (!$this->isKnownClass($className)) { return null; @@ -142,11 +168,17 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p $className, null, $methodName, - $docComment + $docComment, ); if (!isset($this->knownMethodsParameterNames[$className][$methodName])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + if ($className !== $implementingClassName && $resolvedPhpDoc->getNullableNameScope() !== null) { + $resolvedPhpDoc = $resolvedPhpDoc->withNameScope( + $resolvedPhpDoc->getNullableNameScope()->withClassName($implementingClassName), + ); } $methodParameterNames = $this->knownMethodsParameterNames[$className][$methodName]; @@ -164,7 +196,11 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p return null; } - public function findFunctionPhpDoc(string $functionName): ?ResolvedPhpDocBlock + /** + * @param array $positionalParameterNames + * @throws ShouldNotHappenException + */ + public function findFunctionPhpDoc(string $functionName, array $positionalParameterNames): ?ResolvedPhpDocBlock { if (!$this->isKnownFunction($functionName)) { return null; @@ -176,14 +212,29 @@ public function findFunctionPhpDoc(string $functionName): ?ResolvedPhpDocBlock if (array_key_exists($functionName, $this->knownFunctionsDocComments)) { [$file, $docComment] = $this->knownFunctionsDocComments[$functionName]; - $this->functionMap[$functionName] = $this->fileTypeMapper->getResolvedPhpDoc( + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $file, null, null, $functionName, - $docComment + $docComment, ); + if (!isset($this->knownFunctionParameterNames[$functionName])) { + throw new ShouldNotHappenException(); + } + + $functionParameterNames = $this->knownFunctionParameterNames[$functionName]; + $parameterNameMapping = []; + foreach ($positionalParameterNames as $i => $parameterName) { + if (!array_key_exists($i, $functionParameterNames)) { + continue; + } + $parameterNameMapping[$functionParameterNames[$i]] = $parameterName; + } + + $this->functionMap[$functionName] = $resolvedPhpDoc->changeParameterNamesByMapping($parameterNameMapping); + return $this->functionMap[$functionName]; } @@ -215,7 +266,7 @@ private function isKnownFunction(string $functionName): bool private function initializeKnownElements(): void { if ($this->initializing) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($this->initialized) { return; @@ -223,15 +274,17 @@ private function initializeKnownElements(): void $this->initializing = true; - foreach ($this->stubFiles as $stubFile) { - $nodes = $this->parser->parseFile($stubFile); - foreach ($nodes as $node) { - $this->initializeKnownElementNode($stubFile, $node); + try { + foreach ($this->stubFilesProvider->getStubFiles() as $stubFile) { + $nodes = $this->parser->parseFile($stubFile); + foreach ($nodes as $node) { + $this->initializeKnownElementNode($stubFile, $node); + } } + } finally { + $this->initializing = false; + $this->initialized = true; } - - $this->initializing = false; - $this->initialized = true; } private function initializeKnownElementNode(string $stubFile, Node $node): void @@ -250,11 +303,19 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void $this->functionMap[$functionName] = null; return; } + $this->knownFunctionParameterNames[$functionName] = 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; + }, $node->getParams()); + $this->knownFunctionsDocComments[$functionName] = [$stubFile, $docComment->getText()]; return; } - if (!$node instanceof Class_ && !$node instanceof Interface_ && !$node instanceof Trait_) { + if (!$node instanceof Class_ && !$node instanceof Interface_ && !$node instanceof Trait_ && !$node instanceof Node\Stmt\Enum_) { return; } @@ -272,7 +333,9 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void $this->methodMap[$className] = []; $this->propertyMap[$className] = []; + $this->constantMap[$className] = []; $this->knownPropertiesDocComments[$className] = []; + $this->knownConstantsDocComments[$className] = []; $this->knownMethodsDocComments[$className] = []; foreach ($node->stmts as $stmt) { @@ -285,6 +348,14 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void } $this->knownPropertiesDocComments[$className][$property->name->toString()] = [$stubFile, $docComment->getText()]; } + } elseif ($stmt instanceof Node\Stmt\ClassConst) { + foreach ($stmt->consts as $const) { + if ($docComment === null) { + $this->constantMap[$className][$const->name->toString()] = null; + continue; + } + $this->knownConstantsDocComments[$className][$const->name->toString()] = [$stubFile, $docComment->getText()]; + } } elseif ($stmt instanceof Node\Stmt\ClassMethod) { if ($docComment === null) { $this->methodMap[$className][$stmt->name->toString()] = null; @@ -295,7 +366,7 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void $this->knownMethodsDocComments[$className][$methodName] = [$stubFile, $docComment->getText()]; $this->knownMethodsParameterNames[$className][$methodName] = array_map(static function (Node\Param $param): string { if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $param->var->name; diff --git a/src/PhpDoc/StubSourceLocatorFactory.php b/src/PhpDoc/StubSourceLocatorFactory.php index 3eef4391d1..6eca6b0bab 100644 --- a/src/PhpDoc/StubSourceLocatorFactory.php +++ b/src/PhpDoc/StubSourceLocatorFactory.php @@ -2,59 +2,51 @@ namespace PHPStan\PhpDoc; -use PHPStan\DependencyInjection\Container; +use PhpParser\Parser; +use PHPStan\BetterReflection\SourceLocator\Ast\Locator; +use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; +use PHPStan\BetterReflection\SourceLocator\Type\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 Roave\BetterReflection\Reflector\FunctionReflector; -use Roave\BetterReflection\SourceLocator\Ast\Locator; -use Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; -use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - -class StubSourceLocatorFactory -{ - - private \PhpParser\Parser $parser; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; +use function dirname; - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository; - - private \PHPStan\DependencyInjection\Container $container; - - /** @var string[] */ - private array $stubFiles; +final class StubSourceLocatorFactory +{ - /** - * @param string[] $stubFiles - */ public function __construct( - \PhpParser\Parser $parser, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - Container $container, - array $stubFiles + private Parser $php8Parser, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private StubFilesProvider $stubFilesProvider, ) { - $this->parser = $parser; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->container = $container; - $this->stubFiles = $stubFiles; } public function create(): SourceLocator { $locators = []; - $astLocator = new Locator($this->parser, function (): FunctionReflector { - return $this->container->getService('stubFunctionReflector'); - }); - foreach ($this->stubFiles as $stubFile) { + $astPhp8Locator = new Locator($this->php8Parser); + foreach ($this->stubFilesProvider->getStubFiles() as $stubFile) { $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($stubFile); } - $locators[] = new PhpInternalSourceLocator($astLocator, $this->phpStormStubsSourceStubber); + $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 aac6ec7558..b0737cbcc9 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -2,24 +2,53 @@ namespace PHPStan\PhpDoc; +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\Rules\ClassCaseSensitivityCheck; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; +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; @@ -27,84 +56,126 @@ 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; +use function array_fill_keys; +use function count; +use function sprintf; -class StubValidator +final class StubValidator { - /** @var string[] */ - private array $stubFiles; - - private \PHPStan\DependencyInjection\DerivativeContainerFactory $derivativeContainerFactory; - - /** - * @param string[] $stubFiles - */ public function __construct( - array $stubFiles, - DerivativeContainerFactory $derivativeContainerFactory + private DerivativeContainerFactory $derivativeContainerFactory, ) { - $this->stubFiles = $stubFiles; - $this->derivativeContainerFactory = $derivativeContainerFactory; } /** - * @return \PHPStan\Analyser\Error[] + * @param string[] $stubFiles + * @return list */ - public function validate(): array + public function validate(array $stubFiles, bool $debug): array { - $originalBroker = Broker::getInstance(); + if (count($stubFiles) === 0) { + return []; + } + + $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($this->stubFiles); + $nodeScopeResolver->setAnalysedFiles($stubFiles); + + $pathRoutingParser = $container->getService('pathRoutingParser'); + $pathRoutingParser->setAnalysedFiles($stubFiles); - $analysedFiles = array_fill_keys($this->stubFiles, true); + $analysedFiles = array_fill_keys($stubFiles, true); $errors = []; - foreach ($this->stubFiles as $stubFile) { - $tmpErrors = $fileAnalyser->analyseFile( - $stubFile, - $analysedFiles, - $ruleRegistry, - static function (): void { + foreach ($stubFiles as $stubFile) { + try { + $tmpErrors = $fileAnalyser->analyseFile( + $stubFile, + $analysedFiles, + $ruleRegistry, + $collectorRegistry, + static function (): void { + }, + )->getErrors(); + foreach ($tmpErrors as $tmpError) { + $errors[] = $tmpError->withoutTip()->doNotIgnore(); } - )->getErrors(); - foreach ($tmpErrors as $tmpError) { - $errors[] = $tmpError->withoutTip(); + } catch (Throwable $e) { + if ($debug) { + throw $e; + } + + $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); + $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); @@ -112,40 +183,91 @@ 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); - return new Registry([ + $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, true, false), + 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($fileTypeMapper, $genericAncestorsCheck), - new ClassTemplateTypeRule($fileTypeMapper, $templateTypeCheck), + new ClassAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), + new ClassTemplateTypeRule($templateTypeCheck), new FunctionTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new FunctionSignatureVarianceRule($varianceCheck), - new InterfaceAncestorsRule($fileTypeMapper, $genericAncestorsCheck), - new InterfaceTemplateTypeRule($fileTypeMapper, $templateTypeCheck), + new InterfaceAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), + new InterfaceTemplateTypeRule($templateTypeCheck), new MethodTemplateTypeRule($fileTypeMapper, $templateTypeCheck), + new MethodTagTemplateTypeRule($methodTagTemplateTypeCheck), new MethodSignatureVarianceRule($varianceCheck), new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck), - new IncompatiblePhpDocTypeRule( - $fileTypeMapper, - $genericObjectTypeCheck - ), - new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck), + new IncompatiblePhpDocTypeRule($fileTypeMapper, new IncompatiblePhpDocTypeCheck($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper)), + new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), - $container->getByType(PhpDocParser::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), @@ -153,7 +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 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 934b126265..9bc036e1d8 100644 --- a/src/PhpDoc/Tag/DeprecatedTag.php +++ b/src/PhpDoc/Tag/DeprecatedTag.php @@ -2,14 +2,14 @@ namespace PHPStan\PhpDoc\Tag; -class DeprecatedTag +/** + * @api + */ +final class DeprecatedTag { - private ?string $message; - - public function __construct(?string $message) + public function __construct(private ?string $message) { - $this->message = $message; } public function getMessage(): ?string @@ -17,15 +17,4 @@ public function getMessage(): ?string return $this->message; } - /** - * @param mixed[] $properties - * @return DeprecatedTag - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['message'] - ); - } - } diff --git a/src/PhpDoc/Tag/ExtendsTag.php b/src/PhpDoc/Tag/ExtendsTag.php index 8b13e38dbb..72cb97f7cf 100644 --- a/src/PhpDoc/Tag/ExtendsTag.php +++ b/src/PhpDoc/Tag/ExtendsTag.php @@ -4,14 +4,14 @@ use PHPStan\Type\Type; -class ExtendsTag +/** + * @api + */ +final class ExtendsTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -19,15 +19,4 @@ public function getType(): Type return $this->type; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'] - ); - } - } diff --git a/src/PhpDoc/Tag/ImplementsTag.php b/src/PhpDoc/Tag/ImplementsTag.php index f0c70f2c14..556959b68d 100644 --- a/src/PhpDoc/Tag/ImplementsTag.php +++ b/src/PhpDoc/Tag/ImplementsTag.php @@ -4,14 +4,14 @@ use PHPStan\Type\Type; -class ImplementsTag +/** + * @api + */ +final class ImplementsTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -19,15 +19,4 @@ public function getType(): Type return $this->type; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'] - ); - } - } diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index 5a72291c08..43bda4cf97 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -4,30 +4,23 @@ use PHPStan\Type\Type; -class MethodTag +/** + * @api + */ +final class MethodTag { - private \PHPStan\Type\Type $returnType; - - private bool $isStatic; - - /** @var array */ - private array $parameters; - /** - * @param \PHPStan\Type\Type $returnType - * @param bool $isStatic - * @param array $parameters + * @param array $parameters + * @param array $templateTags */ public function __construct( - Type $returnType, - bool $isStatic, - array $parameters + private Type $returnType, + private bool $isStatic, + private array $parameters, + private array $templateTags, ) { - $this->returnType = $returnType; - $this->isStatic = $isStatic; - $this->parameters = $parameters; } public function getReturnType(): Type @@ -41,7 +34,7 @@ public function isStatic(): bool } /** - * @return array + * @return array */ public function getParameters(): array { @@ -49,16 +42,11 @@ public function getParameters(): array } /** - * @param mixed[] $properties - * @return self + * @return array */ - public static function __set_state(array $properties): self + public function getTemplateTags(): array { - return new self( - $properties['returnType'], - $properties['isStatic'], - $properties['parameters'] - ); + return $this->templateTags; } } diff --git a/src/PhpDoc/Tag/MethodTagParameter.php b/src/PhpDoc/Tag/MethodTagParameter.php index 7ee095e60b..3e4c817bf8 100644 --- a/src/PhpDoc/Tag/MethodTagParameter.php +++ b/src/PhpDoc/Tag/MethodTagParameter.php @@ -5,32 +5,20 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -class MethodTagParameter +/** + * @api + */ +final class MethodTagParameter { - private \PHPStan\Type\Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $isOptional; - - private bool $isVariadic; - - private ?\PHPStan\Type\Type $defaultValue; - public function __construct( - Type $type, - PassedByReference $passedByReference, - bool $isOptional, - bool $isVariadic, - ?Type $defaultValue + private Type $type, + private PassedByReference $passedByReference, + private bool $isOptional, + private bool $isVariadic, + private ?Type $defaultValue, ) { - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->isOptional = $isOptional; - $this->isVariadic = $isVariadic; - $this->defaultValue = $defaultValue; } public function getType(): Type @@ -58,19 +46,4 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'], - $properties['passedByReference'], - $properties['isOptional'], - $properties['isVariadic'], - $properties['defaultValue'] - ); - } - } diff --git a/src/PhpDoc/Tag/MixinTag.php b/src/PhpDoc/Tag/MixinTag.php index 368d486860..c115c2cacb 100644 --- a/src/PhpDoc/Tag/MixinTag.php +++ b/src/PhpDoc/Tag/MixinTag.php @@ -4,14 +4,14 @@ use PHPStan\Type\Type; -class MixinTag +/** + * @api + */ +final class MixinTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -19,15 +19,4 @@ public function getType(): Type return $this->type; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['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 e72a687d2a..498dd64ce7 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -4,17 +4,17 @@ use PHPStan\Type\Type; -class ParamTag implements TypedTag +/** + * @api + */ +final class ParamTag implements TypedTag { - private \PHPStan\Type\Type $type; - - private bool $isVariadic; - - public function __construct(Type $type, bool $isVariadic) + public function __construct( + private Type $type, + private bool $isVariadic, + ) { - $this->type = $type; - $this->isVariadic = $isVariadic; } public function getType(): Type @@ -27,25 +27,9 @@ public function isVariadic(): bool return $this->isVariadic; } - /** - * @param Type $type - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type, $this->isVariadic); } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'], - $properties['isVariadic'] - ); - } - } diff --git a/src/PhpDoc/Tag/PropertyTag.php b/src/PhpDoc/Tag/PropertyTag.php index 631fe82e6b..16090c44b0 100644 --- a/src/PhpDoc/Tag/PropertyTag.php +++ b/src/PhpDoc/Tag/PropertyTag.php @@ -4,52 +4,43 @@ use PHPStan\Type\Type; -class PropertyTag +/** + * @api + */ +final class PropertyTag { - private \PHPStan\Type\Type $type; - - private bool $readable; - - private bool $writable; - public function __construct( - Type $type, - bool $readable, - bool $writable + private ?Type $readableType, + private ?Type $writableType, ) { - $this->type = $type; - $this->readable = $readable; - $this->writable = $writable; } - public function getType(): Type + public function getReadableType(): ?Type { - return $this->type; + return $this->readableType; } - public function isReadable(): bool + public function getWritableType(): ?Type { - return $this->readable; + return $this->writableType; } - public function isWritable(): bool + /** + * @phpstan-assert-if-true !null $this->getReadableType() + */ + public function isReadable(): bool { - return $this->writable; + return $this->readableType !== null; } /** - * @param mixed[] $properties - * @return PropertyTag + * @phpstan-assert-if-true !null $this->getWritableType() */ - public static function __set_state(array $properties): self + public function isWritable(): bool { - return new self( - $properties['type'], - $properties['readable'], - $properties['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 c9636fad66..b501dd67e1 100644 --- a/src/PhpDoc/Tag/ReturnTag.php +++ b/src/PhpDoc/Tag/ReturnTag.php @@ -4,17 +4,14 @@ use PHPStan\Type\Type; -class ReturnTag implements TypedTag +/** + * @api + */ +final class ReturnTag implements TypedTag { - private \PHPStan\Type\Type $type; - - private bool $isExplicit; - - public function __construct(Type $type, bool $isExplicit) + public function __construct(private Type $type, private bool $isExplicit) { - $this->type = $type; - $this->isExplicit = $isExplicit; } public function getType(): Type @@ -27,11 +24,7 @@ public function isExplicit(): bool return $this->isExplicit; } - /** - * @param Type $type - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type, $this->isExplicit); } @@ -41,16 +34,4 @@ public function toImplicit(): self return new self($this->type, false); } - /** - * @param mixed[] $properties - * @return ReturnTag - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'], - $properties['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 8180fdd6b4..bafa555833 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -5,22 +5,22 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; -class TemplateTag +/** + * @api + */ +final class TemplateTag { - private string $name; - - private \PHPStan\Type\Type $bound; - - private TemplateTypeVariance $variance; - - public function __construct(string $name, Type $bound, TemplateTypeVariance $variance) + /** + * @param non-empty-string $name + */ + public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance) { - $this->name = $name; - $this->bound = $bound; - $this->variance = $variance; } + /** + * @return non-empty-string + */ public function getName(): string { return $this->name; @@ -31,22 +31,14 @@ public function getBound(): Type return $this->bound; } - public function getVariance(): TemplateTypeVariance + public function getDefault(): ?Type { - return $this->variance; + return $this->default; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self + public function getVariance(): TemplateTypeVariance { - return new self( - $properties['name'], - $properties['bound'], - $properties['variance'] - ); + return $this->variance; } } diff --git a/src/PhpDoc/Tag/ThrowsTag.php b/src/PhpDoc/Tag/ThrowsTag.php index faa7fb2dde..1c1e30b897 100644 --- a/src/PhpDoc/Tag/ThrowsTag.php +++ b/src/PhpDoc/Tag/ThrowsTag.php @@ -4,14 +4,14 @@ use PHPStan\Type\Type; -class ThrowsTag +/** + * @api + */ +final class ThrowsTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -19,15 +19,4 @@ public function getType(): Type return $this->type; } - /** - * @param mixed[] $properties - * @return ThrowsTag - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'] - ); - } - } diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php new file mode 100644 index 0000000000..ab074d6775 --- /dev/null +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -0,0 +1,28 @@ +importedAlias; + } + + public function getImportedFrom(): string + { + return $this->importedFrom; + } + + public function getImportedAs(): ?string + { + return $this->importedAs; + } + +} diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php new file mode 100644 index 0000000000..d5cd10e5d6 --- /dev/null +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -0,0 +1,36 @@ +aliasName; + } + + public function getTypeAlias(): TypeAlias + { + return new TypeAlias( + $this->typeNode, + $this->nameScope, + ); + } + +} diff --git a/src/PhpDoc/Tag/TypedTag.php b/src/PhpDoc/Tag/TypedTag.php index 843fcb16b8..0be9218dce 100644 --- a/src/PhpDoc/Tag/TypedTag.php +++ b/src/PhpDoc/Tag/TypedTag.php @@ -4,13 +4,13 @@ use PHPStan\Type\Type; +/** @api */ interface TypedTag { public function getType(): Type; /** - * @param Type $type * @return static */ public function withType(Type $type): self; diff --git a/src/PhpDoc/Tag/UsesTag.php b/src/PhpDoc/Tag/UsesTag.php index 3d75976b58..1679997ed3 100644 --- a/src/PhpDoc/Tag/UsesTag.php +++ b/src/PhpDoc/Tag/UsesTag.php @@ -4,14 +4,14 @@ use PHPStan\Type\Type; -class UsesTag +/** + * @api + */ +final class UsesTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -19,15 +19,4 @@ public function getType(): Type return $this->type; } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'] - ); - } - } diff --git a/src/PhpDoc/Tag/VarTag.php b/src/PhpDoc/Tag/VarTag.php index c39a0abfce..85c26f1b6c 100644 --- a/src/PhpDoc/Tag/VarTag.php +++ b/src/PhpDoc/Tag/VarTag.php @@ -4,14 +4,14 @@ use PHPStan\Type\Type; -class VarTag implements TypedTag +/** + * @api + */ +final class VarTag implements TypedTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -19,24 +19,9 @@ public function getType(): Type return $this->type; } - /** - * @param Type $type - * @return self - */ - public function withType(Type $type): TypedTag + public function withType(Type $type): self { return new self($type); } - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['type'] - ); - } - } diff --git a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php b/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php deleted file mode 100644 index cb940f021d..0000000000 --- a/src/PhpDoc/TypeAlias/TypeAliasesTypeNodeResolverExtension.php +++ /dev/null @@ -1,78 +0,0 @@ - */ - private array $aliases; - - /** @var array */ - private array $resolvedTypes = []; - - /** @var array */ - private array $inProcess = []; - - /** - * @param TypeStringResolver $typeStringResolver - * @param ReflectionProvider $reflectionProvider - * @param array $aliases - */ - public function __construct( - TypeStringResolver $typeStringResolver, - ReflectionProvider $reflectionProvider, - array $aliases - ) - { - $this->typeStringResolver = $typeStringResolver; - $this->reflectionProvider = $reflectionProvider; - $this->aliases = $aliases; - } - - public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type - { - if ($typeNode instanceof IdentifierTypeNode) { - $aliasName = $typeNode->name; - if (array_key_exists($aliasName, $this->resolvedTypes)) { - return $this->resolvedTypes[$aliasName]; - } - if (!array_key_exists($aliasName, $this->aliases)) { - return null; - } - - if ($this->reflectionProvider->hasClass($aliasName)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); - } - - if (array_key_exists($aliasName, $this->inProcess)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); - } - - $this->inProcess[$aliasName] = true; - - $aliasTypeString = $this->aliases[$aliasName]; - $aliasType = $this->typeStringResolver->resolve($aliasTypeString); - $this->resolvedTypes[$aliasName] = $aliasType; - - unset($this->inProcess[$aliasName]); - - return $aliasType; - } - return null; - } - -} diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index ffa07af055..6abb943d73 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -2,9 +2,15 @@ namespace PHPStan\PhpDoc; +use Closure; +use Generator; +use Iterator; +use IteratorAggregate; use Nette\Utils\Strings; +use PhpParser\Node\Name; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; -use PHPStan\DependencyInjection\Container; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -17,66 +23,124 @@ 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; use PHPStan\Type\BooleanType; 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; +use PHPStan\Type\TypeAliasResolver; +use PHPStan\Type\TypeAliasResolverProvider; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; +use PHPStan\Type\ValueOfType; use PHPStan\Type\VoidType; - -class TypeNodeResolver +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 str_starts_with; +use function strtolower; +use function substr; + +final class TypeNodeResolver { - private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider; - - private Container $container; + /** @var array */ + private array $genericTypeResolvingStack = []; public function __construct( - TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, - Container $container + private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private TypeAliasResolverProvider $typeAliasResolverProvider, + private ConstantResolver $constantResolver, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { - $this->extensionRegistryProvider = $extensionRegistryProvider; - $this->container = $container; } + /** @api */ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type { foreach ($this->extensionRegistryProvider->getRegistry()->getExtensions() as $extension) { @@ -101,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); @@ -112,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(); @@ -123,20 +199,157 @@ 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': + return IntegerRangeType::fromInterval(1, null); + + 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()]); + case 'class-string': + case 'interface-string': + 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()]); + case 'array-key': return new BenevolentUnionType([new IntegerType(), new StringType()]); + case 'scalar': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + + return new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]); + + case '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); + + if ($type !== null) { + return $type; + } + + return new UnionType([new IntegerType(), new FloatType()]); + + case 'numeric': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + + return new UnionType([ + new IntegerType(), + new FloatType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + ]); + + case 'numeric-string': + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + + case 'non-empty-string': + return new IntersectionType([ + new StringType(), + 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(); + case 'boolean': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + return new BooleanType(); case 'true': @@ -149,36 +362,103 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new NullType(); case 'float': + return new FloatType(); + case 'double': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + return new FloatType(); case 'array': + case 'associative-array': return new ArrayType(new MixedType(), new MixedType()); + case 'non-empty-array': + return TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ); + case 'iterable': return new IterableType(new MixedType(), new MixedType()); 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); + + if ($type !== null) { + return $type; + } + + 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 NonAcceptingNeverType(); + case 'never-return': - return new NeverType(true); + case 'never-returns': + case 'no-return': + 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(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) { @@ -187,12 +467,17 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new ObjectType($nameScope->getClassName()); case 'static': - return new StaticType($nameScope->getClassName()); + if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); + + return new StaticType($classReflection); + } + return new ErrorType(); case 'parent': if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); - if ($classReflection->getParentClass() !== false) { + if ($classReflection->getParentClass() !== null) { return new ObjectType($classReflection->getParentClass()->getName()); } } @@ -201,19 +486,70 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } } + if (!$nameScope->shouldBypassTypeAliases()) { + $typeAlias = $this->getTypeAliasResolver()->resolveTypeAlias($typeNode->name, $nameScope); + if ($typeAlias !== null) { + return $typeAlias; + } + } + $templateType = $nameScope->resolveTemplateTypeName($typeNode->name); if ($templateType !== null) { return $templateType; } $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)) { + return new ObjectType($nameScope->resolveStringName($typeNode->name)); + } + + if ($nameScope->getNamespace() === null) { + return null; + } + + $className = $nameScope->resolveStringName($typeNode->name); + + if ($this->getReflectionProvider()->hasClass($className)) { + return new ObjectType($className); + } + + return null; + } + private function resolveThisTypeNode(ThisTypeNode $typeNode, NameScope $nameScope): Type { $className = $nameScope->getClassName(); @@ -228,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 @@ -255,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 { @@ -282,30 +620,90 @@ 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); - return new ArrayType(new MixedType(), $itemType); + return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $itemType); } private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type { $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') { + if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) { if (count($genericTypes) === 1) { // array - return new ArrayType(new MixedType(true), $genericTypes[0]); - + $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); + } elseif (count($genericTypes) === 2) { // array + $keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([ + new IntegerType(), + new StringType(), + ]))->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(); } - if (count($genericTypes) === 2) { // array - return new ArrayType($genericTypes[0], $genericTypes[1]); + if ($mainTypeName === 'non-empty-array') { + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } - } elseif ($mainTypeName === 'list') { + return $arrayType; + } elseif (in_array($mainTypeName, ['list', 'non-empty-list'], true)) { if (count($genericTypes) === 1) { // list - return 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()); + } + + return $listType; } return new ErrorType(); @@ -318,98 +716,240 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 2) { // iterable return new IterableType($genericTypes[0], $genericTypes[1]); } - } elseif ($mainTypeName === 'class-string') { + } elseif (in_array($mainTypeName, ['class-string', 'interface-string'], true)) { if (count($genericTypes) === 1) { $genericType = $genericTypes[0]; - if ((new ObjectWithoutClassType())->isSuperTypeOf($genericType)->yes() || $genericType instanceof MixedType) { + 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> + + if ($genericTypes[0] instanceof ConstantIntegerType) { + $min = $genericTypes[0]->getValue(); + } elseif ($typeNode->genericTypes[0] instanceof IdentifierTypeNode && $typeNode->genericTypes[0]->name === 'min') { + $min = null; + } else { + return new ErrorType(); + } + + if ($genericTypes[1] instanceof ConstantIntegerType) { + $max = $genericTypes[1]->getValue(); + } elseif ($typeNode->genericTypes[1] instanceof IdentifierTypeNode && $typeNode->genericTypes[1]->name === 'max') { + $max = null; + } else { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval($min, $max); + } + } elseif ($mainTypeName === 'key-of') { + if (count($genericTypes) === 1) { // key-of + $type = new KeyOfType($genericTypes[0]); + return $type->isResolvable() ? $type->resolve() : $type; + } + + return new ErrorType(); + } elseif ($mainTypeName === 'value-of') { + if (count($genericTypes) === 1) { // value-of + $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(); + } elseif ($mainTypeName === '__benevolent') { + 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(), [ - \Traversable::class, - \IteratorAggregate::class, - \Iterator::class, + $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(); @@ -417,32 +957,84 @@ 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 (str_starts_with($parameterName, '$')) { + $parameterName = substr($parameterName, 1); + } + return new NativeParameterReflection( - $parameterNode->parameterName, - $parameterNode->isOptional, + $parameterName, + $parameterNode->isOptional || $parameterNode->isVariadic, $this->resolve($parameterNode->type, $nameScope), $parameterNode->isReference ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $parameterNode->isVariadic, - null + null, ); }, - $typeNode->parameters - ); + $typeNode->parameters, + )); + $returnType = $this->resolve($typeNode->returnType, $nameScope); 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 + && $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(); @@ -451,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; @@ -461,19 +1056,55 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } elseif ($itemNode->keyName instanceof ConstExprStringNode) { $offsetType = new ConstantStringType($itemNode->keyName->value); } elseif ($itemNode->keyName !== null) { - throw new \PHPStan\ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); + throw new ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); } $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); } - 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 { $constExpr = $typeNode->constExpr; if ($constExpr instanceof ConstExprArrayNode) { - throw new \PHPStan\ShouldNotHappenException(); // we prefer array shapes + throw new ShouldNotHappenException(); // we prefer array shapes } if ( @@ -481,12 +1112,12 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc || $constExpr instanceof ConstExprTrueNode || $constExpr instanceof ConstExprNullNode ) { - throw new \PHPStan\ShouldNotHappenException(); // we prefer IdentifierTypeNode + throw new ShouldNotHappenException(); // we prefer IdentifierTypeNode } if ($constExpr instanceof ConstFetchNode) { if ($constExpr->className === '') { - throw new \PHPStan\ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode + throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode } if ($nameScope->getClassName() !== null) { @@ -499,13 +1130,14 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc case 'parent': if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); - if ($classReflection->getParentClass() === false) { + if ($classReflection->getParentClass() === null) { return new ErrorType(); } $className = $classReflection->getParentClass()->getName(); } + break; } } @@ -520,15 +1152,32 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc $classReflection = $this->getReflectionProvider()->getClass($className); $constantName = $constExpr->name; - if (Strings::endsWith($constantName, '*')) { - $constantNameStartsWith = Strings::substring($constantName, 0, Strings::length($constantName) - 1); + if (Strings::contains($constantName, '*')) { + // convert * into .*? and escape everything else so the constants can be matched against the pattern + $pattern = '{^' . str_replace('\\*', '.*?', preg_quote($constantName)) . '$}D'; $constantTypes = []; - foreach ($classReflection->getNativeReflection()->getConstants() as $classConstantName => $constantValue) { - if (!Strings::startsWith($classConstantName, $constantNameStartsWith)) { + foreach ($classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) { + $classConstantName = $reflectionConstant->getName(); + if (Strings::match($classConstantName, $pattern) === null) { + continue; + } + + if ($classReflection->isEnum() && $classReflection->hasEnumCase($classConstantName)) { + $constantTypes[] = new EnumCaseObjectType($classReflection->getName(), $classConstantName); 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) { @@ -542,28 +1191,82 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new ErrorType(); } - return $classReflection->getConstant($constantName)->getValueType(); + if ($classReflection->isEnum() && $classReflection->hasEnumCase($constantName)) { + return new EnumCaseObjectType($classReflection->getName(), $constantName); + } + + $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 - * @param NameScope $nameScope - * @return Type[] + * @return list */ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array { @@ -577,7 +1280,12 @@ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array private function getReflectionProvider(): ReflectionProvider { - return $this->container->getByType(ReflectionProvider::class); + return $this->reflectionProviderProvider->getReflectionProvider(); + } + + private function getTypeAliasResolver(): TypeAliasResolver + { + return $this->typeAliasResolverProvider->getTypeAliasResolver(); } } diff --git a/src/PhpDoc/TypeNodeResolverAwareExtension.php b/src/PhpDoc/TypeNodeResolverAwareExtension.php index 38a65f172d..e2ab05a4a3 100644 --- a/src/PhpDoc/TypeNodeResolverAwareExtension.php +++ b/src/PhpDoc/TypeNodeResolverAwareExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDoc; +/** @api */ interface TypeNodeResolverAwareExtension { diff --git a/src/PhpDoc/TypeNodeResolverExtension.php b/src/PhpDoc/TypeNodeResolverExtension.php index f478253017..de004ecbba 100644 --- a/src/PhpDoc/TypeNodeResolverExtension.php +++ b/src/PhpDoc/TypeNodeResolverExtension.php @@ -6,6 +6,23 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Type; +/** + * 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 new file mode 100644 index 0000000000..a525078b2e --- /dev/null +++ b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php @@ -0,0 +1,33 @@ +setTypeNodeResolver($typeNodeResolver); + } + } + + /** + * @return TypeNodeResolverExtension[] + */ + public function getExtensions(): array + { + return $this->extensions; + } + +} diff --git a/src/PhpDoc/TypeNodeResolverExtensionRegistry.php b/src/PhpDoc/TypeNodeResolverExtensionRegistry.php index 8b74ad1e50..93883a32ea 100644 --- a/src/PhpDoc/TypeNodeResolverExtensionRegistry.php +++ b/src/PhpDoc/TypeNodeResolverExtensionRegistry.php @@ -2,36 +2,12 @@ namespace PHPStan\PhpDoc; -class TypeNodeResolverExtensionRegistry +interface TypeNodeResolverExtensionRegistry { - /** @var TypeNodeResolverExtension[] */ - private array $extensions; - - /** - * @param TypeNodeResolverExtension[] $extensions - */ - public function __construct( - TypeNodeResolver $typeNodeResolver, - array $extensions - ) - { - foreach ($extensions as $extension) { - if (!$extension instanceof TypeNodeResolverAwareExtension) { - continue; - } - - $extension->setTypeNodeResolver($typeNodeResolver); - } - $this->extensions = $extensions; - } - /** * @return TypeNodeResolverExtension[] */ - public function getExtensions(): array - { - return $this->extensions; - } + public function getExtensions(): array; } diff --git a/src/PhpDoc/TypeStringResolver.php b/src/PhpDoc/TypeStringResolver.php index 1f868b9cba..2bdb4ff94f 100644 --- a/src/PhpDoc/TypeStringResolver.php +++ b/src/PhpDoc/TypeStringResolver.php @@ -8,27 +8,19 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\Type\Type; -class TypeStringResolver +final class TypeStringResolver { - private Lexer $typeLexer; - - private TypeParser $typeParser; - - private TypeNodeResolver $typeNodeResolver; - - public function __construct(Lexer $typeLexer, TypeParser $typeParser, TypeNodeResolver $typeNodeResolver) + public function __construct(private Lexer $typeLexer, private TypeParser $typeParser, private TypeNodeResolver $typeNodeResolver) { - $this->typeLexer = $typeLexer; - $this->typeParser = $typeParser; - $this->typeNodeResolver = $typeNodeResolver; } + /** @api */ public function resolve(string $typeString, ?NameScope $nameScope = null): Type { $tokens = new TokenIterator($this->typeLexer->tokenize($typeString)); $typeNode = $this->typeParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); + $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 new file mode 100644 index 0000000000..2fd49e7cfa --- /dev/null +++ b/src/Process/CpuCoreCounter.php @@ -0,0 +1,28 @@ +count !== null) { + return $this->count; + } + + try { + $this->count = (new FidryCpuCoreCounter())->getCount(); + } catch (NumberOfCpuCoreNotFound) { + $this->count = 1; + } + + return $this->count; + } + +} diff --git a/src/Process/ProcessCanceledException.php b/src/Process/ProcessCanceledException.php new file mode 100644 index 0000000000..ae42c75d3b --- /dev/null +++ b/src/Process/ProcessCanceledException.php @@ -0,0 +1,10 @@ +getOption('memory-limit') === null) { + $processCommandArray[] = '-d'; + $processCommandArray[] = 'memory_limit=' . ini_get('memory_limit'); + } + + foreach ([$mainScript, $commandName] as $arg) { + $processCommandArray[] = escapeshellarg($arg); + } + + if ($projectConfigFile !== null) { + $processCommandArray[] = '--configuration'; + $processCommandArray[] = escapeshellarg($projectConfigFile); + } + + $options = [ + AnalyseCommand::OPTION_LEVEL, + 'autoload-file', + 'memory-limit', + 'xdebug', + 'verbose', + ]; + foreach ($options as $optionName) { + /** @var bool|string|null $optionValue */ + $optionValue = $input->getOption($optionName); + if (is_bool($optionValue)) { + if ($optionValue === true) { + $processCommandArray[] = sprintf('--%s', $optionName); + } + continue; + } + if ($optionValue === null) { + continue; + } + + $processCommandArray[] = sprintf('--%s=%s', $optionName, escapeshellarg($optionValue)); + } + + $processCommandArray = array_merge($processCommandArray, $additionalItems); + + $processCommandArray[] = '--'; + + /** @var string[] $paths */ + $paths = $input->getArgument('paths'); + foreach ($paths as $path) { + $processCommandArray[] = escapeshellarg($path); + } + + return implode(' ', $processCommandArray); + } + +} diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php new file mode 100644 index 0000000000..31f975460a --- /dev/null +++ b/src/Process/ProcessPromise.php @@ -0,0 +1,98 @@ + */ + private Deferred $deferred; + + private ?Process $process = null; + + private bool $canceled = false; + + public function __construct(private LoopInterface $loop, private string $name, private string $command) + { + $this->deferred = new Deferred(); + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return PromiseInterface + */ + public function run(): PromiseInterface + { + $tmpStdOutResource = tmpfile(); + if ($tmpStdOutResource === false) { + throw new ShouldNotHappenException('Failed creating temp file for stdout.'); + } + $tmpStdErrResource = tmpfile(); + if ($tmpStdErrResource === false) { + throw new ShouldNotHappenException('Failed creating temp file for stderr.'); + } + + $this->process = new Process($this->command, null, null, [ + 1 => $tmpStdOutResource, + 2 => $tmpStdErrResource, + ]); + $this->process->start($this->loop); + + $this->process->on('exit', function ($exitCode) use ($tmpStdOutResource, $tmpStdErrResource): void { + if ($this->canceled) { + fclose($tmpStdOutResource); + fclose($tmpStdErrResource); + return; + } + rewind($tmpStdOutResource); + $stdOut = stream_get_contents($tmpStdOutResource); + fclose($tmpStdOutResource); + + rewind($tmpStdErrResource); + $stdErr = stream_get_contents($tmpStdErrResource); + fclose($tmpStdErrResource); + + if ($exitCode === null) { + $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); + return; + } + + if ($exitCode === 0) { + if ($stdOut === false) { + $stdOut = ''; + } + $this->deferred->resolve($stdOut); + return; + } + + $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); + }); + + return $this->deferred->promise(); + } + + public function cancel(): void + { + if ($this->process === null) { + throw new ShouldNotHappenException('Cancelling process before running'); + } + $this->canceled = true; + $this->process->terminate(); + $this->deferred->reject(new ProcessCanceledException()); + } + +} diff --git a/src/Reflection/AdditionalConstructorsExtension.php b/src/Reflection/AdditionalConstructorsExtension.php new file mode 100644 index 0000000000..ad9995b64c --- /dev/null +++ b/src/Reflection/AdditionalConstructorsExtension.php @@ -0,0 +1,29 @@ + + */ + public function getAllowedSubTypes(ClassReflection $classReflection): array; + +} diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index c63f2c4108..696e0e5b08 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -2,56 +2,38 @@ 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\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 { - private string $name; - - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private Type $returnType; - - private bool $isStatic; - - /** @var \PHPStan\Reflection\Annotations\AnnotationsMethodParameterReflection[] */ - private array $parameters; - - private bool $isVariadic; - - /** @var FunctionVariant[]|null */ + /** @var list|null */ private ?array $variants = null; /** - * @param string $name - * @param ClassReflection $declaringClass - * @param Type $returnType - * @param \PHPStan\Reflection\Annotations\AnnotationsMethodParameterReflection[] $parameters - * @param bool $isStatic - * @param bool $isVariadic + * @param list $parameters */ public function __construct( - string $name, - ClassReflection $declaringClass, - Type $returnType, - array $parameters, - bool $isStatic, - bool $isVariadic + private string $name, + private ClassReflection $declaringClass, + private Type $returnType, + private array $parameters, + private bool $isStatic, + private bool $isVariadic, + private ?Type $throwType, + private TemplateTypeMap $templateTypeMap, ) { - $this->name = $name; - $this->declaringClass = $declaringClass; - $this->returnType = $returnType; - $this->parameters = $parameters; - $this->isStatic = $isStatic; - $this->isVariadic = $isVariadic; } public function getDeclaringClass(): ClassReflection @@ -84,25 +66,34 @@ public function getName(): string return $this->name; } - /** - * @return \PHPStan\Reflection\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, + $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(); @@ -118,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(); } @@ -138,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 acbaaaac09..8f4f322d08 100644 --- a/src/Reflection/Annotations/AnnotationPropertyReflection.php +++ b/src/Reflection/Annotations/AnnotationPropertyReflection.php @@ -3,32 +3,30 @@ 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 { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \PHPStan\Type\Type $type; - - private bool $readable; - - private bool $writable; - public function __construct( - ClassReflection $declaringClass, - Type $type, - bool $readable = true, - bool $writable = true + private string $name, + private ClassReflection $declaringClass, + private Type $readableType, + private Type $writableType, + private bool $readable, + private bool $writable, ) { - $this->declaringClass = $declaringClass; - $this->type = $type; - $this->readable = $readable; - $this->writable = $writable; + } + + public function getName(): string + { + return $this->name; } public function getDeclaringClass(): ClassReflection @@ -51,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 @@ -96,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 40d9c42981..b01a6db6ff 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,33 +2,17 @@ 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 { - private string $name; - - private Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $isOptional; - - private bool $isVariadic; - - private ?Type $defaultValue; - - public function __construct(string $name, Type $type, PassedByReference $passedByReference, bool $isOptional, bool $isVariadic, ?Type $defaultValue) + public function __construct(private string $name, private Type $type, private PassedByReference $passedByReference, private bool $isOptional, private bool $isVariadic, private ?Type $defaultValue) { - $this->name = $name; - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->isOptional = $isOptional; - $this->isVariadic = $isVariadic; - $this->defaultValue = $defaultValue; } public function getName(): string @@ -46,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; @@ -61,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 2ca4782d02..9ad470b0ee 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -2,101 +2,125 @@ 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\FileTypeMapper; - -class AnnotationsMethodsClassReflectionExtension implements 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; + +final class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension { - private FileTypeMapper $fileTypeMapper; - - /** @var MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $methods = []; - public function __construct(FileTypeMapper $fileTypeMapper) - { - $this->fileTypeMapper = $fileTypeMapper; - } - public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - if (!isset($this->methods[$classReflection->getName()])) { - $this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $classReflection); + if (!isset($this->methods[$classReflection->getCacheKey()][$methodName])) { + $method = $this->findClassReflectionWithMethod($classReflection, $classReflection, $methodName); + if ($method === null) { + return false; + } + $this->methods[$classReflection->getCacheKey()][$methodName] = $method; } - return isset($this->methods[$classReflection->getName()][$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->getName()][$methodName]; + return $this->methods[$classReflection->getCacheKey()][$methodName]; } - /** - * @param ClassReflection $classReflection - * @param ClassReflection $declaringClass - * @return MethodReflection[] - */ - private function createMethods( + private function findClassReflectionWithMethod( ClassReflection $classReflection, - ClassReflection $declaringClass - ): array + ClassReflection $declaringClass, + string $methodName, + ): ?ExtendedMethodReflection { - $methods = []; - foreach ($classReflection->getTraits() as $traitClass) { - $methods += $this->createMethods($traitClass, $classReflection); - } - foreach ($classReflection->getParents() as $parentClass) { - $methods += $this->createMethods($parentClass, $parentClass); - foreach ($parentClass->getTraits() as $traitClass) { - $methods += $this->createMethods($traitClass, $parentClass); - } - } - foreach ($classReflection->getInterfaces() as $interfaceClass) { - $methods += $this->createMethods($interfaceClass, $interfaceClass); - } - - $fileName = $classReflection->getFileName(); - if ($fileName === false) { - return $methods; - } - - $docComment = $classReflection->getNativeReflection()->getDocComment(); - if ($docComment === false) { - return $methods; - } - - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $classReflection->getName(), null, null, $docComment); - foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) { + $methodTags = $classReflection->getMethodTags(); + if (isset($methodTags[$methodName])) { $parameters = []; - foreach ($methodTag->getParameters() as $parameterName => $parameterTag) { + foreach ($methodTags[$methodName]->getParameters() as $parameterName => $parameterTag) { $parameters[] = new AnnotationsMethodParameterReflection( $parameterName, $parameterTag->getType(), $parameterTag->passedByReference(), $parameterTag->isOptional(), $parameterTag->isVariadic(), - $parameterTag->getDefaultValue() + $parameterTag->getDefaultValue(), ); } - $methods[$methodName] = new AnnotationMethodReflection( + $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, - $methodTag->getReturnType(), + TemplateTypeHelper::resolveTemplateTypes( + $methodTags[$methodName]->getReturnType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ), $parameters, - $methodTag->isStatic(), - $this->detectMethodVariadic($parameters) + $isStatic, + $this->detectMethodVariadic($parameters), + $classReflection->hasNativeMethod($nativeCallMethodName) + ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() + : null, + $templateTypeMap, ); } - return $methods; + + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findClassReflectionWithMethod($traitClass, $classReflection, $methodName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; + } + + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { + $methodWithDeclaringClass = $this->findClassReflectionWithMethod($parentClass, $parentClass, $methodName); + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; + } + + $parentClass = $parentClass->getParentClass(); + } + + foreach ($classReflection->getInterfaces() as $interfaceClass) { + $methodWithDeclaringClass = $this->findClassReflectionWithMethod($interfaceClass, $interfaceClass, $methodName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; + } + + return null; } /** * @param AnnotationsMethodParameterReflection[] $parameters - * @return bool */ private function detectMethodVariadic(array $parameters): bool { diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index 748563ec6e..5c19c29ae7 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -3,83 +3,103 @@ namespace PHPStan\Reflection\Annotations; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; -use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\NeverType; -class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - /** @var \PHPStan\Reflection\PropertyReflection[][] */ + /** @var ExtendedPropertyReflection[][] */ private array $properties = []; - public function __construct(FileTypeMapper $fileTypeMapper) - { - $this->fileTypeMapper = $fileTypeMapper; - } - public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - if (!isset($this->properties[$classReflection->getName()])) { - $this->properties[$classReflection->getName()] = $this->createProperties($classReflection, $classReflection); + if (!isset($this->properties[$classReflection->getCacheKey()][$propertyName])) { + $property = $this->findClassReflectionWithProperty($classReflection, $classReflection, $propertyName); + if ($property === null) { + return false; + } + $this->properties[$classReflection->getCacheKey()][$propertyName] = $property; } - return isset($this->properties[$classReflection->getName()][$propertyName]); + 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->getName()][$propertyName]; + return $this->properties[$classReflection->getCacheKey()][$propertyName]; } - /** - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param \PHPStan\Reflection\ClassReflection $declaringClass - * @return \PHPStan\Reflection\PropertyReflection[] - */ - private function createProperties( + private function findClassReflectionWithProperty( ClassReflection $classReflection, - ClassReflection $declaringClass - ): array + ClassReflection $declaringClass, + string $propertyName, + ): ?ExtendedPropertyReflection { - $properties = []; - foreach ($classReflection->getTraits() as $traitClass) { - $properties += $this->createProperties($traitClass, $classReflection); - } - foreach ($classReflection->getParents() as $parentClass) { - $properties += $this->createProperties($parentClass, $parentClass); - foreach ($parentClass->getTraits() as $traitClass) { - $properties += $this->createProperties($traitClass, $parentClass); + $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(); } - } - foreach ($classReflection->getInterfaces() as $interfaceClass) { - $properties += $this->createProperties($interfaceClass, $interfaceClass); + return new AnnotationPropertyReflection( + $propertyName, + $declaringClass, + TemplateTypeHelper::resolveTemplateTypes( + $propertyTag->getReadableType() ?? new NeverType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ), + TemplateTypeHelper::resolveTemplateTypes( + $propertyTag->getWritableType() ?? new NeverType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ), + $isReadable, + $isWritable, + ); } - $fileName = $classReflection->getFileName(); - if ($fileName === false) { - return $properties; + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findClassReflectionWithProperty($traitClass, $classReflection, $propertyName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; } - $docComment = $classReflection->getNativeReflection()->getDocComment(); - if ($docComment === false) { - return $properties; + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { + $methodWithDeclaringClass = $this->findClassReflectionWithProperty($parentClass, $parentClass, $propertyName); + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; + } + + $parentClass = $parentClass->getParentClass(); } - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $classReflection->getName(), null, null, $docComment); - foreach ($resolvedPhpDoc->getPropertyTags() as $propertyName => $propertyTag) { - $properties[$propertyName] = new AnnotationPropertyReflection( - $declaringClass, - $propertyTag->getType(), - $propertyTag->isReadable(), - $propertyTag->isWritable() - ); + foreach ($classReflection->getInterfaces() as $interfaceClass) { + $methodWithDeclaringClass = $this->findClassReflectionWithProperty($interfaceClass, $interfaceClass, $propertyName); + if ($methodWithDeclaringClass === null) { + continue; + } + + return $methodWithDeclaringClass; } - return $properties; + return null; } } 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 a4654d5277..707aeca28a 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -2,103 +2,104 @@ namespace PHPStan\Reflection\BetterReflection; -use PhpParser\PrettyPrinter\Standard; +use Closure; +use Nette\Utils\Strings; +use PhpParser\Node; 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\Adapter\ReflectionParameter; +use PHPStan\BetterReflection\Reflection\ReflectionEnum; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\BetterReflection\Reflector\Reflector; +use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\Broker\AnonymousClassNameHelper; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\ConstantNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\File\FileHelper; +use PHPStan\File\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\Type\ConstantTypeHelper; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\MixedType; use PHPStan\Type\Type; -use Roave\BetterReflection\Identifier\Exception\InvalidIdentifierName; -use Roave\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use Roave\BetterReflection\Reflection\Adapter\ReflectionClass; -use Roave\BetterReflection\Reflection\Adapter\ReflectionFunction; -use Roave\BetterReflection\Reflector\ClassReflector; -use Roave\BetterReflection\Reflector\ConstantReflector; -use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; -use Roave\BetterReflection\Reflector\FunctionReflector; -use Roave\BetterReflection\SourceLocator\Located\LocatedSource; - -class BetterReflectionProvider implements ReflectionProvider +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; + +final class BetterReflectionProvider implements ReflectionProvider { - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - private \PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider; - - private \Roave\BetterReflection\Reflector\ClassReflector $classReflector; - - private \Roave\BetterReflection\Reflector\FunctionReflector $functionReflector; - - private \Roave\BetterReflection\Reflector\ConstantReflector $constantReflector; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider $nativeFunctionReflectionProvider; - - private StubPhpDocProvider $stubPhpDocProvider; - - private \PHPStan\Reflection\FunctionReflectionFactory $functionReflectionFactory; - - private RelativePathHelper $relativePathHelper; - - private AnonymousClassNameHelper $anonymousClassNameHelper; - - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\File\FileHelper $fileHelper; - - /** @var \PHPStan\Reflection\FunctionReflection[] */ + /** @var FunctionReflection[] */ private array $functionReflections = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private array $classReflections = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private static array $anonymousClasses = []; + /** @var array */ + private array $cachedConstants = []; + + /** + * @param list $universalObjectCratesClasses + */ public function __construct( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - ClassReflector $classReflector, - FileTypeMapper $fileTypeMapper, - NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, - StubPhpDocProvider $stubPhpDocProvider, - FunctionReflectionFactory $functionReflectionFactory, - RelativePathHelper $relativePathHelper, - AnonymousClassNameHelper $anonymousClassNameHelper, - Standard $printer, - FileHelper $fileHelper, - FunctionReflector $functionReflector, - ConstantReflector $constantReflector + 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 FileHelper $fileHelper, + private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private SignatureMapProvider $signatureMapProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + private array $universalObjectCratesClasses, ) { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->classReflector = $classReflector; - $this->fileTypeMapper = $fileTypeMapper; - $this->nativeFunctionReflectionProvider = $nativeFunctionReflectionProvider; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->functionReflectionFactory = $functionReflectionFactory; - $this->relativePathHelper = $relativePathHelper; - $this->anonymousClassNameHelper = $anonymousClassNameHelper; - $this->printer = $printer; - $this->fileHelper = $fileHelper; - $this->functionReflector = $functionReflector; - $this->constantReflector = $constantReflector; } public function hasClass(string $className): bool @@ -107,12 +108,16 @@ public function hasClass(string $className): bool return true; } + if (!ClassNameHelper::isValidClassName($className)) { + return false; + } + try { - $this->classReflector->reflect($className); + $this->reflector->reflectClass($className); return true; - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { return false; - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { return false; } } @@ -124,9 +129,9 @@ public function getClass(string $className): ClassReflection } try { - $reflectionClass = $this->classReflector->reflect($className); - } catch (IdentifierNotFound $e) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + $reflectionClass = $this->reflector->reflectClass($className); + } catch (IdentifierNotFound | InvalidIdentifierName) { + throw new ClassNotFoundException($className); } $reflectionClassName = strtolower($reflectionClass->getName()); @@ -135,16 +140,29 @@ public function getClass(string $className): ClassReflection return $this->classReflections[$reflectionClassName]; } + $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true); + $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(), - new ReflectionClass($reflectionClass), + $reflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($reflectionClass) : new ReflectionClass($reflectionClass), null, null, - $this->stubPhpDocProvider->findClassPhpDoc($className) + $this->stubPhpDocProvider->findClassPhpDoc($reflectionClass->getName()), + $this->universalObjectCratesClasses, ); $this->classReflections[$reflectionClassName] = $classReflection; @@ -155,29 +173,29 @@ public function getClass(string $className): ClassReflection public function getClassName(string $className): string { if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]->getDisplayName(); } - $reflectionClass = $this->classReflector->reflect($className); + $reflectionClass = $this->reflector->reflectClass($className); return $reflectionClass->getName(); } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { if (isset($classNode->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$scope->isInTrait()) { $scopeFile = $scope->getFile(); } else { $scopeFile = $scope->getTraitReflection()->getFileName(); - if ($scopeFile === false) { + if ($scopeFile === null) { $scopeFile = $scope->getFile(); } } @@ -185,48 +203,83 @@ public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNo $filename = $this->fileHelper->normalizePath($this->relativePathHelper->getRelativePath($scopeFile), '/'); $className = $this->anonymousClassNameHelper->getAnonymousClassName( $classNode, - $scopeFile + $scopeFile, ); - $classNode->name = new \PhpParser\Node\Identifier($className); - $classNode->setAttribute('anonymousClass', true); + $classNode->name = new Node\Identifier($className); + $classNode->namespacedName = null; if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]; } - $reflectionClass = \Roave\BetterReflection\Reflection\ReflectionClass::createFromNode( - $this->classReflector, + $reflectionClass = \PHPStan\BetterReflection\Reflection\ReflectionClass::createFromNode( + $this->reflector, $classNode, - new LocatedSource($this->printer->prettyPrint([$classNode]), $scopeFile), - null + 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->stubPhpDocProvider->findClassPhpDoc($className), + $this->universalObjectCratesClasses, ); $this->classReflections[$className] = self::$anonymousClasses[$className]; return self::$anonymousClasses[$className]; } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function getUniversalObjectCratesClasses(): array { - return $this->resolveFunctionName($nameNode, $scope) !== null; + return $this->universalObjectCratesClasses; } - public function getFunction(\PhpParser\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 \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); + throw new FunctionNotFoundException((string) $nameNode); } $lowerCasedFunctionName = strtolower($functionName); @@ -234,6 +287,10 @@ public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): Func 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; @@ -245,126 +302,183 @@ public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): Func return $this->functionReflections[$lowerCasedFunctionName]; } - private function getCustomFunction(string $functionName): \PHPStan\Reflection\Php\PhpFunctionReflection + private function getCustomFunction(string $functionName): PhpFunctionReflection { - $reflectionFunction = new ReflectionFunction($this->functionReflector->reflect($functionName)); + $reflectionFunction = new ReflectionFunction($this->reflector->reflectFunction($functionName)); $templateTypeMap = TemplateTypeMap::createEmpty(); - $phpDocParameterTags = []; + $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; - $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName()); + $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) { - $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($reflectionFunction->getFileName(), null, null, $reflectionFunction->getName(), $docComment); } if ($resolvedPhpDoc !== null) { $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 function (ParamTag $paramTag): Type { - return $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() + $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(\PhpParser\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->functionReflector->reflect($name); + $this->reflector->reflectFunction($name); return true; - } catch (\Roave\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { // pass } + if ($this->nativeFunctionReflectionProvider->findFunctionReflection($name) !== null) { + return $this->phpstormStubsSourceStubber->isPresentFunction($name) !== false; + } return false; - }, $scope); + }, $namespaceAnswerer); } - public function hasConstant(\PhpParser\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(\PhpParser\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 \PHPStan\Broker\ConstantNotFoundException((string) $nameNode); + throw new ConstantNotFoundException((string) $nameNode); } - $constantReflection = $this->constantReflector->reflect($constantName); - try { - $constantValue = $constantReflection->getValue(); - $constantValueType = ConstantTypeHelper::getTypeFromValue($constantValue); - $fileName = $constantReflection->getFileName(); - } catch (UnableToCompileNode $e) { - $constantValueType = new MixedType(); - $fileName = null; + if (array_key_exists($constantName, $this->cachedConstants)) { + return $this->cachedConstants[$constantName]; + } + + $constantReflection = $this->reflector->reflectConstant($constantName); + $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 new RuntimeConstantReflection( + return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( $constantName, $constantValueType, - $fileName + $fileName, + TrinaryLogic::createFromBoolean($isDeprecated), + $deprecatedDescription, ); } - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string { return $this->resolveName($nameNode, function (string $name): bool { try { - $this->constantReflector->reflect($name); + $this->reflector->reflectConstant($name); return true; - } catch (\Roave\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode $e) { + } catch (UnableToCompileNode) { // pass } return false; - }, $scope); + }, $namespaceAnswerer); } /** - * @param \PhpParser\Node\Name $nameNode - * @param \Closure(string $name): bool $existsCallback - * @param Scope|null $scope - * @return string|null + * @param Closure(string $name): bool $existsCallback */ private function resolveName( - \PhpParser\Node\Name $nameNode, - \Closure $existsCallback, - ?Scope $scope + Node\Name $nameNode, + Closure $existsCallback, + ?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/BetterReflectionProviderFactory.php b/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php index 2b5c7db77c..b6fa9c0309 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php @@ -2,17 +2,13 @@ namespace PHPStan\Reflection\BetterReflection; -use Roave\BetterReflection\Reflector\ClassReflector; -use Roave\BetterReflection\Reflector\ConstantReflector; -use Roave\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\Reflector; interface BetterReflectionProviderFactory { public function create( - FunctionReflector $functionReflector, - ClassReflector $classReflector, - ConstantReflector $constantReflector + Reflector $reflector, ): BetterReflectionProvider; } diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index e192922550..8632d6b59c 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -2,74 +2,38 @@ namespace PHPStan\Reflection\BetterReflection; -use PHPStan\DependencyInjection\Container; +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\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 Roave\BetterReflection\Reflector\FunctionReflector; -use Roave\BetterReflection\SourceLocator\Ast\Locator; -use Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; -use Roave\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; -use Roave\BetterReflection\SourceLocator\SourceStubber\SourceStubber; -use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\EvaledCodeSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - -class BetterReflectionSourceLocatorFactory -{ - - /** @var \PhpParser\Parser */ - private $parser; - - /** @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 \PHPStan\DependencyInjection\Container */ - private $container; +use function array_merge; +use function array_unique; +use function extension_loaded; +use function is_dir; +use function is_file; - /** @var string[] */ - private $autoloadDirectories; - - /** @var string[] */ - private $autoloadFiles; - - /** @var string[] */ - private $scanFiles; - - /** @var string[] */ - private $scanDirectories; - - /** @var string[] */ - private $analysedPaths; - - /** @var string[] */ - private $composerAutoloaderProjectPaths; - - /** @var string[] */ - private $analysedPathsFromConfig; +final class BetterReflectionSourceLocatorFactory +{ /** - * @param string[] $autoloadDirectories - * @param string[] $autoloadFiles * @param string[] $scanFiles * @param string[] $scanDirectories * @param string[] $analysedPaths @@ -77,51 +41,37 @@ class BetterReflectionSourceLocatorFactory * @param string[] $analysedPathsFromConfig */ public function __construct( - \PhpParser\Parser $parser, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber, - ReflectionSourceStubber $reflectionSourceStubber, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, - ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, - AutoloadSourceLocator $autoloadSourceLocator, - Container $container, - array $autoloadDirectories, - array $autoloadFiles, - array $scanFiles, - array $scanDirectories, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $analysedPathsFromConfig + 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->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; - $this->reflectionSourceStubber = $reflectionSourceStubber; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; - $this->composerJsonAndInstalledJsonSourceLocatorMaker = $composerJsonAndInstalledJsonSourceLocatorMaker; - $this->autoloadSourceLocator = $autoloadSourceLocator; - $this->container = $container; - $this->autoloadDirectories = $autoloadDirectories; - $this->autoloadFiles = $autoloadFiles; - $this->scanFiles = $scanFiles; - $this->scanDirectories = $scanDirectories; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->analysedPathsFromConfig = $analysedPathsFromConfig; } public function create(): SourceLocator { $locators = []; - foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { - $locator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerAutoloaderProjectPath); - if ($locator === null) { - continue; - } - $locators[] = $locator; - } + $astLocator = new Locator($this->parser); + $locators[] = new AutoloadFunctionsSourceLocator( + new AutoloadSourceLocator($this->fileNodesFetcher, false), + new ReflectionClassSourceLocator( + $astLocator, + $this->reflectionSourceStubber, + ), + ); $analysedDirectories = []; $analysedFiles = []; @@ -139,23 +89,50 @@ public function create(): SourceLocator $analysedDirectories[] = $analysedPath; } - $analysedFiles = array_unique(array_merge($analysedFiles, $this->autoloadFiles, $this->scanFiles)); + $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->autoloadDirectories, $this->scanDirectories)); + $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, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); - $locators[] = new SkipClassAliasSourceLocator(new PhpInternalSourceLocator($astLocator, $this->phpstormStubsSourceStubber)); - $locators[] = $this->autoloadSourceLocator; - $locators[] = new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber); - $locators[] = new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber); + $astPhp8Locator = new Locator($this->php8Parser); + + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $locator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerAutoloaderProjectPath); + if ($locator === null) { + continue; + } + $fileLocators[] = $locator; + } + + 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); return new MemoizingSourceLocator(new AggregateSourceLocator($locators)); } diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php deleted file mode 100644 index 1140407039..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php +++ /dev/null @@ -1,39 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionClass for the specified $className. - * - * @return \Roave\BetterReflection\Reflection\ReflectionClass - * - * @throws \Roave\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $className): Reflection - { - $lowerClassName = strtolower($className); - if (isset($this->reflections[$lowerClassName])) { - if ($this->reflections[$lowerClassName] instanceof \Throwable) { - throw $this->reflections[$lowerClassName]; - } - return $this->reflections[$lowerClassName]; - } - - try { - return $this->reflections[$lowerClassName] = parent::reflect($className); - } catch (\Throwable $e) { - $this->reflections[$lowerClassName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php deleted file mode 100644 index f006d0a427..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php +++ /dev/null @@ -1,38 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionConstant for the specified $constantName. - * - * @return \Roave\BetterReflection\Reflection\ReflectionConstant - * - * @throws \Roave\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $constantName): Reflection - { - if (isset($this->reflections[$constantName])) { - if ($this->reflections[$constantName] instanceof \Throwable) { - throw $this->reflections[$constantName]; - } - return $this->reflections[$constantName]; - } - - try { - return $this->reflections[$constantName] = parent::reflect($constantName); - } catch (\Throwable $e) { - $this->reflections[$constantName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php deleted file mode 100644 index 3eb2b4b5df..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php +++ /dev/null @@ -1,39 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionFunction for the specified $functionName. - * - * @return \Roave\BetterReflection\Reflection\ReflectionFunction - * - * @throws \Roave\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $functionName): Reflection - { - $lowerFunctionName = strtolower($functionName); - if (isset($this->reflections[$lowerFunctionName])) { - if ($this->reflections[$lowerFunctionName] instanceof \Throwable) { - throw $this->reflections[$lowerFunctionName]; - } - return $this->reflections[$lowerFunctionName]; - } - - try { - return $this->reflections[$lowerFunctionName] = parent::reflect($functionName); - } catch (\Throwable $e) { - $this->reflections[$lowerFunctionName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php new file mode 100644 index 0000000000..486ad45c20 --- /dev/null +++ b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php @@ -0,0 +1,111 @@ + */ + private array $classReflections = []; + + /** @var array */ + private array $constantReflections = []; + + /** @var array */ + private array $functionReflections = []; + + public function __construct(private Reflector $reflector) + { + } + + public function reflectClass(string $className): ReflectionClass + { + $lowerClassName = strtolower($className); + if (array_key_exists($lowerClassName, $this->classReflections) && $this->classReflections[$lowerClassName] !== null) { + return $this->classReflections[$lowerClassName]; + } + if (array_key_exists($className, $this->classReflections)) { + $classReflection = $this->classReflections[$className]; + if ($classReflection === null) { + throw IdentifierNotFound::fromIdentifier(new Identifier($className, new IdentifierType(IdentifierType::IDENTIFIER_CLASS))); + } + + return $classReflection; + } + + try { + return $this->classReflections[$lowerClassName] = $this->reflector->reflectClass($className); + } catch (IdentifierNotFound $e) { + $this->classReflections[$className] = null; + + throw $e; + } + } + + public function reflectConstant(string $constantName): ReflectionConstant + { + if (array_key_exists($constantName, $this->constantReflections)) { + $constantReflection = $this->constantReflections[$constantName]; + if ($constantReflection === null) { + throw IdentifierNotFound::fromIdentifier(new Identifier($constantName, new IdentifierType(IdentifierType::IDENTIFIER_CONSTANT))); + } + + return $constantReflection; + } + + try { + return $this->constantReflections[$constantName] = $this->reflector->reflectConstant($constantName); + } catch (IdentifierNotFound $e) { + $this->constantReflections[$constantName] = null; + + throw $e; + } + } + + public function reflectFunction(string $functionName): ReflectionFunction + { + $lowerFunctionName = strtolower($functionName); + if (array_key_exists($lowerFunctionName, $this->functionReflections)) { + $functionReflection = $this->functionReflections[$lowerFunctionName]; + if ($functionReflection === null) { + throw IdentifierNotFound::fromIdentifier(new Identifier($functionName, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION))); + } + + return $functionReflection; + } + + try { + return $this->functionReflections[$lowerFunctionName] = $this->reflector->reflectFunction($functionName); + } catch (IdentifierNotFound $e) { + $this->functionReflections[$lowerFunctionName] = null; + + throw $e; + } + } + + public function reflectAllClasses(): iterable + { + return $this->reflector->reflectAllClasses(); + } + + public function reflectAllFunctions(): iterable + { + return $this->reflector->reflectAllFunctions(); + } + + public function reflectAllConstants(): iterable + { + return $this->reflector->reflectAllConstants(); + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/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 b22325553b..7506682426 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -6,21 +6,38 @@ 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; +use PHPStan\BetterReflection\Reflection\ReflectionConstant; +use PHPStan\BetterReflection\Reflector\Reflector; +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 ReflectionException; use ReflectionFunction; -use Roave\BetterReflection\Identifier\Identifier; -use Roave\BetterReflection\Identifier\IdentifierType; -use Roave\BetterReflection\Reflection\Reflection; -use Roave\BetterReflection\Reflection\ReflectionConstant; -use Roave\BetterReflection\Reflector\Reflector; -use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure; -use Roave\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; -use Roave\BetterReflection\SourceLocator\Located\LocatedSource; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; use function array_key_exists; -use function file_exists; +use function array_keys; +use function class_exists; +use function constant; +use function count; +use function defined; +use function function_exists; +use function interface_exists; +use function is_file; +use function is_string; +use function opcache_invalidate; use function restore_error_handler; +use function set_error_handler; +use function spl_autoload_functions; +use function strtolower; +use function trait_exists; +use const PHP_VERSION_ID; /** * Use PHP's built in autoloader to locate a class, without actually loading. @@ -30,49 +47,33 @@ * * Modified code from Roave/BetterReflection, Copyright (c) 2017 Roave, LLC. */ -class AutoloadSourceLocator implements SourceLocator +final class AutoloadSourceLocator implements SourceLocator { - private FileNodesFetcher $fileNodesFetcher; + /** @var array{classes: array, functions: array, constants: array} */ + private array $presentSymbols = [ + 'classes' => [], + 'functions' => [], + 'constants' => [], + ]; - /** @var array>> */ - private array $classNodes = []; + /** @var array */ + private array $scannedFiles = []; - /** @var array */ - private array $classReflections = []; + /** @var array */ + private array $startLineByClass = []; - /** @var array> */ - private array $functionNodes = []; - - /** @var array> */ - private array $constantNodes = []; - - /** @var array */ - private array $locatedSourcesByFile = []; - - public function __construct(FileNodesFetcher $fileNodesFetcher) + public function __construct(private FileNodesFetcher $fileNodesFetcher, private bool $executeAutoloadersInFileReadTrap) { - $this->fileNodesFetcher = $fileNodesFetcher; } - /** - * {@inheritDoc} - * - * @throws ParseToAstFailure - */ public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { 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->locatedSourcesByFile[$this->functionNodes[$loweredFunctionName]->getFileName()], - $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; @@ -84,73 +85,39 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): if (!is_string($reflectionFileName)) { return null; } + if (!is_file($reflectionFileName)) { + return null; + } return $this->findReflection($reflector, $reflectionFileName, $identifier, null); } 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(), - $this->locatedSourcesByFile[$stmtConst->getFileName()], - $stmtConst->getNamespace() - ); - if ($constantReflection === null) { - continue; - } - if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); - } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; - } - - return $constantReflection; - } - - foreach (array_keys($stmtConst->getNode()->consts) as $i) { - $constantReflection = $nodeToReflection->__invoke( - $reflector, - $stmtConst->getNode(), - $this->locatedSourcesByFile[$stmtConst->getFileName()], - $stmtConst->getNamespace(), - $i - ); - if ($constantReflection === null) { - continue; - } - if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\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('', null), + new LocatedSource('populateValue(constant($constantName)); - - return $reflection; } if (!$identifier->isClass()) { @@ -158,60 +125,95 @@ 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) { return null; } - [$potentiallyLocatedFile, $className, $startLine] = $locateResult; + [$potentiallyLocatedFiles, $className, $startLine] = $locateResult; + if ($startLine !== null) { + $this->startLineByClass[strtolower($className)] = $startLine; + } + + $newIdentifier = new Identifier($className, $identifier->getType()); + + foreach ($potentiallyLocatedFiles as $potentiallyLocatedFile) { + $reflection = $this->findReflection($reflector, $potentiallyLocatedFile, $newIdentifier, $startLine); + if ($reflection === null) { + continue; + } - return $this->findReflection($reflector, $potentiallyLocatedFile, new Identifier($className, $identifier->getType()), $startLine); + return $reflection; + } + + return null; } private function findReflection(Reflector $reflector, string $file, Identifier $identifier, ?int $startLine): ?Reflection { - if (!array_key_exists($file, $this->locatedSourcesByFile)) { - $result = $this->fileNodesFetcher->fetchNodes($file); - $this->locatedSourcesByFile[$file] = $result->getLocatedSource(); - 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; } - $locatedSource = $result->getLocatedSource(); - } else { - $locatedSource = $this->locatedSourcesByFile[$file]; + $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; } - foreach ($this->classNodes[$identifierName] as $classNode) { - if ($startLine !== null && $startLine !== $classNode->getNode()->getStartLine()) { - continue; + $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--; + } + if ($startLine !== $classNode->getNode()->getStartLine()) { + continue; + } } - return $this->classReflections[$identifierName] = $nodeToReflection->__invoke( + return $nodeToReflection->__invoke( $reflector, $classNode->getNode(), - $locatedSource, - $classNode->getNamespace() + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); } @@ -219,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(), - $locatedSource, - $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; @@ -236,7 +280,19 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - return []; // todo + 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; } /** @@ -252,31 +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. * - * @throws ReflectionException - * @return array{string, string, int|null}|null + * @return array{string[], string, int|null}|null */ private function locateClassByName(string $className): ?array { - if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { - $reflection = new ReflectionClass($className); + $reflection = $this->getReflectionClass($className); + if ($reflection !== null) { $filename = $reflection->getFileName(); - if (!is_string($filename)) { return null; } - - if (!file_exists($filename)) { + 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->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) { @@ -292,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(); } @@ -307,9 +376,7 @@ static function () use ($className): ?array { private function silenceErrors(): void { - set_error_handler(static function (): bool { - return true; - }); + set_error_handler(static fn (): bool => true); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 7ff01f954a..6eb1f57604 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -2,100 +2,115 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\BuilderHelpers; +use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; +use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; -use Roave\BetterReflection\Reflection\Exception\InvalidConstantNode; -use Roave\BetterReflection\Util\ConstantNodeChecker; +use PHPStan\BetterReflection\Reflection\Exception\InvalidConstantNode; +use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\BetterReflection\Util\ConstantNodeChecker; +use PHPStan\Reflection\ConstantNameHelper; +use function strtolower; -class CachingVisitor extends NodeVisitorAbstract +final class CachingVisitor extends NodeVisitorAbstract { private string $fileName; - /** @var array>> */ + private string $contents; + + /** @var array>> */ private array $classNodes; - /** @var array> */ + /** @var array>> */ private array $functionNodes; - /** @var array> */ + /** @var array>> */ private array $constantNodes; - private ?\PhpParser\Node\Stmt\Namespace_ $currentNamespaceNode = null; + private ?Node\Stmt\Namespace_ $currentNamespaceNode = null; - public function enterNode(\PhpParser\Node $node): ?int + public function enterNode(Node $node): ?int { if ($node instanceof Namespace_) { $this->currentNamespaceNode = $node; + + return null; } - if ($node instanceof \PhpParser\Node\Stmt\ClassLike) { + if ($node instanceof Node\Stmt\ClassLike) { if ($node->name !== null) { - $this->classNodes[strtolower($node->namespacedName->toString())][] = new FetchedNode( + $fullClassName = $node->name->toString(); + if ($this->currentNamespaceNode !== null && $this->currentNamespaceNode->name !== null) { + $fullClassName = $this->currentNamespaceNode->name . '\\' . $fullClassName; + } + $this->classNodes[strtolower($fullClassName)][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName + new LocatedSource($this->contents, $fullClassName, $this->fileName), ); } - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof \PhpParser\Node\Stmt\Function_) { - $this->functionNodes[strtolower($node->namespacedName->toString())] = new FetchedNode( - $node, - $this->currentNamespaceNode, - $this->fileName - ); + if ($node instanceof Node\Stmt\Function_) { + if ($node->namespacedName !== null) { + $functionName = $node->namespacedName->toString(); + $this->functionNodes[strtolower($functionName)][] = new FetchedNode( + $node, + $this->currentNamespaceNode, + new LocatedSource($this->contents, $functionName, $this->fileName), + ); + } - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof \PhpParser\Node\Stmt\Const_) { - $this->constantNodes[] = new FetchedNode( - $node, - $this->currentNamespaceNode, - $this->fileName - ); + if ($node instanceof Node\Stmt\Const_) { + foreach ($node->consts as $const) { + if ($const->namespacedName === null) { + continue; + } + + $this->constantNodes[ConstantNameHelper::normalize($const->namespacedName->toString())][] = new FetchedNode( + $node, + $this->currentNamespaceNode, + new LocatedSource($this->contents, null, $this->fileName), + ); + } - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof \PhpParser\Node\Expr\FuncCall) { + if ($node instanceof Node\Expr\FuncCall) { try { ConstantNodeChecker::assertValidDefineFunctionCall($node); - } catch (InvalidConstantNode $e) { + } catch (InvalidConstantNode) { return null; } - /** @var \PhpParser\Node\Scalar\String_ $nameNode */ - $nameNode = $node->args[0]->value; + /** @var Node\Scalar\String_ $nameNode */ + $nameNode = $node->getArgs()[0]->value; $constantName = $nameNode->value; - if (defined($constantName)) { - $constantValue = constant($constantName); - $node->args[1]->value = BuilderHelpers::normalizeValue($constantValue); - } - $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 \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; } /** - * @param \PhpParser\Node $node * @return null */ - public function leaveNode(\PhpParser\Node $node) + public function leaveNode(Node $node) { if (!$node instanceof Namespace_) { return null; @@ -106,7 +121,7 @@ public function leaveNode(\PhpParser\Node $node) } /** - * @return array>> + * @return array>> */ public function getClassNodes(): array { @@ -114,7 +129,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array>> */ public function getFunctionNodes(): array { @@ -122,19 +137,20 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array>> */ public function getConstantNodes(): array { return $this->constantNodes; } - public function reset(string $fileName): void + public function reset(string $fileName, string $contents): void { $this->classNodes = []; $this->functionNodes = []; $this->constantNodes = []; $this->fileName = $fileName; + $this->contents = $contents; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index f2eb5b38c5..9e687100f7 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -3,113 +3,164 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use Nette\Utils\Json; +use Nette\Utils\JsonException; +use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; +use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; -use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping; -use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; +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 { - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory; - public function __construct( - OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory + private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory, + private PhpVersion $phpVersion, ) { - $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->optimizedPsrAutoloaderLocatorFactory = $optimizedPsrAutoloaderLocatorFactory; } - public function create(string $installationPath): ?SourceLocator + public function create(string $projectInstallationPath): ?SourceLocator { - $composerJsonPath = $installationPath . '/composer.json'; - if (!is_file($composerJsonPath)) { + $composer = ComposerHelper::getComposerConfig($projectInstallationPath); + + if ($composer === null) { return null; } - $installedJsonPath = $installationPath . '/vendor/composer/installed.json'; + + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($projectInstallationPath, $composer); + + $installedJsonPath = $vendorDirectory . '/composer/installed.json'; if (!is_file($installedJsonPath)) { return null; } - try { - $composerJsonContents = FileReader::read($composerJsonPath); - $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { - return null; - } + $installedJsonDirectoryPath = dirname($installedJsonPath); try { $installedJsonContents = FileReader::read($installedJsonPath); $installedJson = Json::decode($installedJsonContents, Json::FORCE_ARRAY); - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { + } catch (CouldNotReadFileException | JsonException) { return null; } $installed = $installedJson['packages'] ?? $installedJson; + $dev = (bool) ($installedJson['dev'] ?? true); $classMapPaths = array_merge( - $this->prefixPaths($this->packageToClassMapPaths($composer), $installationPath . '/'), - ...array_map(function (array $package) use ($installationPath): array { - return $this->prefixPaths( - $this->packageToClassMapPaths($package), - $this->packagePrefixPath($installationPath, $package) - ); - }, $installed) + $this->prefixPaths($this->packageToClassMapPaths($composer), $projectInstallationPath . '/'), + $dev ? $this->prefixPaths($this->packageToClassMapPaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], + ...array_map(fn (array $package): array => $this->prefixPaths( + $this->packageToClassMapPaths($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), $installationPath . '/'), - ...array_map(function (array $package) use ($installationPath): array { - return $this->prefixPaths( - $this->packageToFilePaths($package), - $this->packagePrefixPath($installationPath, $package) - ); - }, $installed) + $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($installedJsonDirectoryPath, $package, $vendorDirectory), + ), $installed), ); $locators = []; $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( Psr4Mapping::fromArrayMappings(array_merge_recursive( - $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $installationPath), - ...array_map(function (array $package) use ($installationPath): array { - return $this->prefixWithPackagePath( - $this->packageToPsr4AutoloadNamespaces($package), - $installationPath, - $package - ); - }, $installed) - )) + $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $projectInstallationPath), + $dev ? $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], + ...array_map(fn (array $package): array => $this->prefixWithPackagePath( + $this->packageToPsr4AutoloadNamespaces($package), + $installedJsonDirectoryPath, + $package, + $vendorDirectory, + ), $installed), + )), ); $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( Psr0Mapping::fromArrayMappings(array_merge_recursive( - $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $installationPath), - ...array_map(function (array $package) use ($installationPath): array { - return $this->prefixWithPackagePath( - $this->packageToPsr0AutoloadNamespaces($package), - $installationPath, - $package - ); - }, $installed) - )) + $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $projectInstallationPath), + $dev ? $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], + ...array_map(fn (array $package): array => $this->prefixWithPackagePath( + $this->packageToPsr0AutoloadNamespaces($package), + $installedJsonDirectoryPath, + $package, + $vendorDirectory, + ), $installed), + )), ); - foreach ($classMapDirectories as $classMapDirectory) { - $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapDirectory); + $files = []; + foreach ($classMapPaths as $classMapPath) { + if (is_dir($classMapPath)) { + $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapPath); + continue; + } + if (!is_file($classMapPath)) { + continue; + } + $files[] = $classMapPath; + } + foreach ($filePaths as $file) { + if (!is_file($file)) { + continue; + } + $files[] = $file; + } + + if (count($files) > 0) { + $locators[] = $this->optimizedDirectorySourceLocatorFactory->createByFiles($files); } - foreach (array_merge($classMapFiles, $filePaths) as $file) { - $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($file); + $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); @@ -120,11 +171,9 @@ public function create(string $installationPath): ?SourceLocator * * @return array> */ - private function packageToPsr4AutoloadNamespaces(array $package): array + private function packageToPsr4AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static function ($namespacePaths): array { - return (array) $namespacePaths; - }, $package['autoload']['psr-4'] ?? []); + return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-4'] ?? []); } /** @@ -132,11 +181,9 @@ private function packageToPsr4AutoloadNamespaces(array $package): array * * @return array> */ - private function packageToPsr0AutoloadNamespaces(array $package): array + private function packageToPsr0AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static function ($namespacePaths): array { - return (array) $namespacePaths; - }, $package['autoload']['psr-0'] ?? []); + return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-0'] ?? []); } /** @@ -144,9 +191,9 @@ private function packageToPsr0AutoloadNamespaces(array $package): array * * @return array */ - private function packageToClassMapPaths(array $package): array + private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { - return $package['autoload']['classmap'] ?? []; + return $package[$autoloadSection]['classmap'] ?? []; } /** @@ -154,17 +201,25 @@ private function packageToClassMapPaths(array $package): array * * @return array */ - private function packageToFilePaths(array $package): array + private function packageToFilePaths(array $package, string $autoloadSection = 'autoload'): array { - return $package['autoload']['files'] ?? []; + return $package[$autoloadSection]['files'] ?? []; } /** * @param mixed[] $package */ - private function packagePrefixPath(string $trimmedInstallationPath, array $package): string + private function packagePrefixPath( + string $installedJsonDirectoryPath, + array $package, + string $vendorDirectory, + ): string { - return $trimmedInstallationPath . '/vendor/' . $package['name'] . '/'; + if (array_key_exists('install-path', $package)) { + return $installedJsonDirectoryPath . '/' . $package['install-path'] . '/'; + } + + return $vendorDirectory . '/' . $package['name'] . '/'; } /** @@ -173,13 +228,11 @@ private function packagePrefixPath(string $trimmedInstallationPath, array $packa * * @return array> */ - private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package): array + private function prefixWithPackagePath(array $paths, string $installedJsonDirectoryPath, array $package, string $vendorDirectory): array { - $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package); + $prefix = $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory); - return array_map(function (array $paths) use ($prefix): array { - return $this->prefixPaths($paths, $prefix); - }, $paths); + return array_map(fn (array $paths): array => $this->prefixPaths($paths, $prefix), $paths); } /** @@ -189,9 +242,7 @@ private function prefixWithPackagePath(array $paths, string $trimmedInstallation */ private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array { - return array_map(function (array $paths) use ($trimmedInstallationPath): array { - return $this->prefixPaths($paths, $trimmedInstallationPath . '/'); - }, $paths); + return array_map(fn (array $paths): array => $this->prefixPaths($paths, $trimmedInstallationPath . '/'), $paths); } /** @@ -201,9 +252,7 @@ private function prefixWithInstallationPath(array $paths, string $trimmedInstall */ private function prefixPaths(array $paths, string $prefix): array { - return array_map(static function (string $path) use ($prefix): string { - return $prefix . $path; - }, $paths); + return array_map(static fn (string $path): string => $prefix . $path, $paths); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php index c5a48bf720..70eaadffbe 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php @@ -2,51 +2,42 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PhpParser\Node; +use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; + /** - * @template-covariant T of \PhpParser\Node + * @template-covariant T of Node */ -class FetchedNode +final class FetchedNode { - /** @var T */ - private \PhpParser\Node $node; - - private ?\PhpParser\Node\Stmt\Namespace_ $namespace; - - private string $fileName; - /** * @param T $node - * @param \PhpParser\Node\Stmt\Namespace_|null $namespace - * @param string $fileName */ public function __construct( - \PhpParser\Node $node, - ?\PhpParser\Node\Stmt\Namespace_ $namespace, - string $fileName + private Node $node, + private ?Node\Stmt\Namespace_ $namespace, + private LocatedSource $locatedSource, ) { - $this->node = $node; - $this->namespace = $namespace; - $this->fileName = $fileName; } /** * @return T */ - public function getNode(): \PhpParser\Node + public function getNode(): Node { return $this->node; } - public function getNamespace(): ?\PhpParser\Node\Stmt\Namespace_ + public function getNamespace(): ?Node\Stmt\Namespace_ { return $this->namespace; } - public function getFileName(): string + public function getLocatedSource(): LocatedSource { - return $this->fileName; + return $this->locatedSource; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php index e2dda64270..ac90178d49 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php @@ -2,43 +2,26 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use Roave\BetterReflection\SourceLocator\Located\LocatedSource; +use PhpParser\Node; -class FetchedNodesResult +final class FetchedNodesResult { - /** @var array>> */ - private array $classNodes; - - /** @var array> */ - private array $functionNodes; - - /** @var array> */ - private array $constantNodes; - - private \Roave\BetterReflection\SourceLocator\Located\LocatedSource $locatedSource; - /** - * @param array>> $classNodes - * @param array> $functionNodes - * @param array> $constantNodes - * @param \Roave\BetterReflection\SourceLocator\Located\LocatedSource $locatedSource + * @param array>> $classNodes + * @param array>> $functionNodes + * @param array>> $constantNodes */ public function __construct( - array $classNodes, - array $functionNodes, - array $constantNodes, - LocatedSource $locatedSource + private array $classNodes, + private array $functionNodes, + private array $constantNodes, ) { - $this->classNodes = $classNodes; - $this->functionNodes = $functionNodes; - $this->constantNodes = $constantNodes; - $this->locatedSource = $locatedSource; } /** - * @return array>> + * @return array>> */ public function getClassNodes(): array { @@ -46,7 +29,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array>> */ public function getFunctionNodes(): array { @@ -54,16 +37,11 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array>> */ public function getConstantNodes(): array { return $this->constantNodes; } - public function getLocatedSource(): LocatedSource - { - return $this->locatedSource; - } - } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index fe1b748d05..bd6892cf88 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -3,24 +3,18 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PhpParser\NodeTraverser; -use PhpParser\Parser; use PHPStan\File\FileReader; -use Roave\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; -class FileNodesFetcher +final class FileNodesFetcher { - private \PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor $cachingVisitor; - - private Parser $phpParser; - public function __construct( - CachingVisitor $cachingVisitor, - Parser $phpParser + private CachingVisitor $cachingVisitor, + private Parser $parser, ) { - $this->cachingVisitor = $cachingVisitor; - $this->phpParser = $phpParser; } public function fetchNodes(string $fileName): FetchedNodesResult @@ -29,23 +23,24 @@ public function fetchNodes(string $fileName): FetchedNodesResult $nodeTraverser->addVisitor($this->cachingVisitor); $contents = FileReader::read($fileName); - $locatedSource = new LocatedSource($contents, $fileName); try { - /** @var \PhpParser\Node[] $ast */ - $ast = $this->phpParser->parse($contents); - } catch (\PhpParser\Error $e) { - return new FetchedNodesResult([], [], [], $locatedSource); + $ast = $this->parser->parseFile($fileName); + } catch (ParserErrorsException) { + return new FetchedNodesResult([], [], []); } - $this->cachingVisitor->reset($fileName); + $this->cachingVisitor->reset($fileName, $contents); $nodeTraverser->traverse($ast); - return new FetchedNodesResult( + $result = new FetchedNodesResult( $this->cachingVisitor->getClassNodes(), $this->cachingVisitor->getFunctionNodes(), $this->cachingVisitor->getConstantNodes(), - $locatedSource ); + + $this->cachingVisitor->reset($fileName, $contents); + + return $result; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php index 52f58da694..4a35d07ca2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php @@ -2,17 +2,25 @@ 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; +use const SEEK_CUR; +use const SEEK_END; +use const SEEK_SET; +use const STREAM_URL_STAT_QUIET; /** * This class will operate as a stream wrapper, intercepting any access to a file while * in operation. * * @internal DO NOT USE: this is an implementation detail of - * the {@see \Roave\BetterReflection\SourceLocator\Type\AutoloadSourceLocator} + * the {@see \PHPStan\BetterReflection\SourceLocator\Type\AutoloadSourceLocator} * * phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps @@ -29,7 +37,12 @@ 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; + + private int $seekPosition = 0; /** * @param string[] $streamWrapperProtocols @@ -42,11 +55,11 @@ final class FileReadTrapStreamWrapper */ public static function withStreamWrapperOverride( callable $executeMeWithinStreamWrapperOverride, - array $streamWrapperProtocols = self::DEFAULT_STREAM_WRAPPER_PROTOCOLS + array $streamWrapperProtocols = self::DEFAULT_STREAM_WRAPPER_PROTOCOLS, ) { self::$registeredStreamWrapperProtocols = $streamWrapperProtocols; - self::$autoloadLocatedFile = null; + self::$autoloadLocatedFiles = []; try { foreach ($streamWrapperProtocols as $protocol) { @@ -62,7 +75,7 @@ public static function withStreamWrapperOverride( } self::$registeredStreamWrapperProtocols = null; - self::$autoloadLocatedFile = null; + self::$autoloadLocatedFiles = []; return $result; } @@ -84,9 +97,59 @@ 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); - return false; + if ($exists) { + self::$autoloadLocatedFiles[] = $path; + } + $this->readFromFile = false; + $this->seekPosition = 0; + + return $exists; + } + + /** + * Since we allow our wrapper's stream_open() to succeed, we need to + * simulate a successful read so autoloaders with require() don't explode. + * + * @param int $count + * + */ + public function stream_read($count): string + { + $this->readFromFile = true; + + // Dummy return value that is also valid PHP for require(). We'll read + // and process the file elsewhere, so it's OK to provide dummy data for + // this read. + return ''; + } + + /** + * Since we allowed the open to succeed, we should allow the close to occur + * as well. + * + */ + public function stream_close(): void + { + // no op + } + + /** + * Required for `require_once` and `include_once` to work per PHP.net + * comment referenced below. We delegate to url_stat(). + * + * @see https://www.php.net/manual/en/function.stream-wrapper-register.php#51855 + * + * @return mixed[]|bool + */ + public function stream_stat() + { + if (self::$autoloadLocatedFiles === []) { + return false; + } + + return $this->url_stat(self::$autoloadLocatedFiles[0], STREAM_URL_STAT_QUIET); } /** @@ -106,20 +169,31 @@ public function stream_open($path, $mode, $options, &$openedPath): bool * @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 \LogicException(self::class . ' not registered: cannot operate. Do not call this method directly.'); + throw new ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.'); } foreach (self::$registeredStreamWrapperProtocols as $protocol) { 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); @@ -129,4 +203,71 @@ public function url_stat($path, $flags) return $result; } + /** + * Simulates behavior of reading from an empty file. + * + */ + public function stream_eof(): bool + { + return $this->readFromFile; + } + + public function stream_flush(): bool + { + return true; + } + + public function stream_tell(): int + { + return $this->seekPosition; + } + + /** + * @param int $offset + * @param int $whence + */ + public function stream_seek($offset, $whence): bool + { + switch ($whence) { + // Behavior is the same for a zero-length file + case SEEK_SET: + case SEEK_END: + if ($offset < 0) { + return false; + } + $this->seekPosition = $offset; + return true; + + case SEEK_CUR: + if ($offset < 0) { + return false; + } + $this->seekPosition += $offset; + return true; + + default: + return false; + } + } + + /** + * @param int $option + * @param int $arg1 + * @param int $arg2 + */ + 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 3035f6fb81..b56a981bab 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -2,137 +2,125 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\File\FileFinder; -use Roave\BetterReflection\Identifier\Identifier; -use Roave\BetterReflection\Identifier\IdentifierType; -use Roave\BetterReflection\Reflection\Reflection; -use Roave\BetterReflection\Reflector\Reflector; -use Roave\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; +use PhpParser\Node; +use PHPStan\BetterReflection\Identifier\Identifier; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; +use PHPStan\BetterReflection\Reflector\Reflector; +use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; +use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Reflection\ConstantNameHelper; +use PHPStan\ShouldNotHappenException; use function array_key_exists; +use function array_values; +use function current; +use function strtolower; -class OptimizedDirectorySourceLocator implements SourceLocator +final class OptimizedDirectorySourceLocator implements SourceLocator { - private \PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher $fileNodesFetcher; - - private \PHPStan\File\FileFinder $fileFinder; - - private string $directory; - - /** @var array|null */ - private ?array $classToFile = null; - - /** @var array>|null */ - private ?array $functionToFiles = null; - - /** @var array> */ - private array $classNodes = []; - - /** @var array> */ - private array $functionNodes = []; - - /** @var array */ - private array $locatedSourcesByFile = []; - + /** + * @param array $classToFile + * @param array> $functionToFiles + * @param array $constantToFile + */ public function __construct( - FileNodesFetcher $fileNodesFetcher, - FileFinder $fileFinder, - string $directory + private FileNodesFetcher $fileNodesFetcher, + private array $classToFile, + private array $functionToFiles, + private array $constantToFile, ) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileFinder = $fileFinder; - $this->directory = $directory; } 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); - $locatedSource = $fetchedNodesResult->getLocatedSource(); - $this->locatedSourcesByFile[$file] = $locatedSource; - 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)) { - throw new \PHPStan\ShouldNotHappenException(); + 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); - $locatedSource = $fetchedNodesResult->getLocatedSource(); - $this->locatedSourcesByFile[$file] = $locatedSource; - 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 Reflector $reflector - * @param FetchedNode<\PhpParser\Node\Stmt\ClassLike>|FetchedNode<\PhpParser\Node\Stmt\Function_> $fetchedNode - * @return Reflection + * @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(); - $reflection = $nodeToReflection->__invoke( + return $nodeToReflection->__invoke( $reflector, $fetchedNode->getNode(), - $this->locatedSourcesByFile[$fetchedNode->getFileName()], - $fetchedNode->getNamespace() + $fetchedNode->getLocatedSource(), + $fetchedNode->getNamespace(), + $positionInNode, ); - - if ($reflection === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $reflection; } private function findFileByClass(string $className): ?string { - if ($this->classToFile === null) { - $this->init(); - if ($this->classToFile === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - } - if (!array_key_exists($className, $this->classToFile)) { return null; } @@ -140,19 +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]; + } + /** - * @param string $functionName * @return string[] */ private function findFilesByFunction(string $functionName): array { - if ($this->functionToFiles === null) { - $this->init(); - if ($this->functionToFiles === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - } - if (!array_key_exists($functionName, $this->functionToFiles)) { return []; } @@ -160,110 +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 { - $fileFinderResult = $this->fileFinder->findFiles([$this->directory]); - $classToFile = []; - $functionToFiles = []; - foreach ($fileFinderResult->getFiles() 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 - * - * @param string $file - * @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 (!preg_match('{\b(?:class|interface|trait|function)\s}i', $contents)) { - return ['classes' => [], 'functions' => []]; + if ($constantNode instanceof Node\Expr\FuncCall) { + return null; } - // strip heredocs/nowdocs - $contents = preg_replace('{<<<[ \t]*([\'"]?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)(?:\s*)\\2(?=\s+|[;,.)])}s', 'null', $contents); - // strip strings - $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); - // strip leading non-php code if needed - if (substr($contents, 0, 2) !== ' [], 'functions' => []]; + /** @var int $position */ + foreach ($constantNode->consts as $position => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); } - } - // strip non-php blocks in the file - $contents = preg_replace('{\?>(?:[^<]++|<(?!\?))*+<\?}s', '?>'); - if ($pos !== false && strpos(substr($contents, $pos), '])(?Pclass|interface|trait|function) \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) - | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] - ) - }ix', $contents, $matches); - - $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, '\\')); - - if ($matches['type'][$i] === 'function') { - $functions[] = $namespacedName; - } else { - $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 []; // todo + throw new ShouldNotHappenException(); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index 03042bb473..620284912f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -2,9 +2,215 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -interface OptimizedDirectorySourceLocatorFactory +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; + +final class OptimizedDirectorySourceLocatorFactory { - public function create(string $directory): OptimizedDirectorySourceLocator; + 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, + $classToFile, + $functionToFiles, + $constantToFile, + ); + } + + /** + * @param string[] $files + */ + 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, + $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 48c56ec40d..f71d4dcf8a 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php @@ -2,17 +2,16 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -class OptimizedDirectorySourceLocatorRepository -{ +use function array_key_exists; - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory $factory; +final class OptimizedDirectorySourceLocatorRepository +{ /** @var array */ private array $locators = []; - public function __construct(OptimizedDirectorySourceLocatorFactory $factory) + public function __construct(private OptimizedDirectorySourceLocatorFactory $factory) { - $this->factory = $factory; } public function getOrCreate(string $directory): OptimizedDirectorySourceLocator @@ -21,7 +20,7 @@ public function getOrCreate(string $directory): OptimizedDirectorySourceLocator return $this->locators[$directory]; } - $this->locators[$directory] = $this->factory->create($directory); + $this->locators[$directory] = $this->factory->createByDirectory($directory); return $this->locators[$directory]; } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index 7b67698360..78fb07f24d 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -2,41 +2,51 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use Roave\BetterReflection\Identifier\Identifier; -use Roave\BetterReflection\Identifier\IdentifierType; -use Roave\BetterReflection\Reflection\Reflection; -use Roave\BetterReflection\Reflector\Reflector; -use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - -class OptimizedPsrAutoloaderLocator implements SourceLocator -{ +use PHPStan\BetterReflection\Identifier\Identifier; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; +use PHPStan\BetterReflection\Reflector\Reflector; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; +use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use function is_file; - private PsrAutoloaderMapping $mapping; +final class OptimizedPsrAutoloaderLocator implements SourceLocator +{ - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository; + /** @var array */ + private array $locators = []; public function __construct( - PsrAutoloaderMapping $mapping, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository + private PsrAutoloaderMapping $mapping, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, ) { - $this->mapping = $mapping; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { + foreach ($this->locators as $locator) { + $reflection = $locator->locateIdentifier($reflector, $identifier); + if ($reflection === null) { + continue; + } + + return $reflection; + } + foreach ($this->mapping->resolvePossibleFilePaths($identifier) as $file) { - if (!file_exists($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; } @@ -44,11 +54,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @return Reflection[] + * @return list */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - return []; // todo + return []; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocatorFactory.php index 36f31e94a7..4a466b7e2d 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocatorFactory.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; interface OptimizedPsrAutoloaderLocatorFactory { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php index 1303aad368..4285d93bbd 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -2,43 +2,79 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\Node\Expr\FuncCall; -use Roave\BetterReflection\Identifier\Identifier; -use Roave\BetterReflection\Identifier\IdentifierType; -use Roave\BetterReflection\Reflection\Reflection; -use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionConstant; -use Roave\BetterReflection\Reflection\ReflectionFunction; -use Roave\BetterReflection\Reflector\Reflector; -use Roave\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - -class OptimizedSingleFileSourceLocator implements SourceLocator -{ - - private \PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher $fileNodesFetcher; +use PhpParser\Node\Stmt\Const_; +use PHPStan\BetterReflection\Identifier\Identifier; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; +use PHPStan\BetterReflection\Reflection\ReflectionClass; +use PHPStan\BetterReflection\Reflection\ReflectionConstant; +use PHPStan\BetterReflection\Reflection\ReflectionFunction; +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; - private string $fileName; +final class OptimizedSingleFileSourceLocator implements SourceLocator +{ - private ?\PHPStan\Reflection\BetterReflection\SourceLocator\FetchedNodesResult $fetchedNodesResult = null; + /** @var array{classes: array, functions: array, constants: array}|null */ + private ?array $presentSymbols = null; public function __construct( - FileNodesFetcher $fileNodesFetcher, - string $fileName + private FileNodesFetcher $fileNodesFetcher, + private string $fileName, ) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileName = $fileName; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { - 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; @@ -48,11 +84,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $classReflection = $nodeToReflection->__invoke( $reflector, $classNode->getNode(), - $this->fetchedNodesResult->getLocatedSource(), - $classNode->getNamespace() + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); if (!$classReflection instanceof ReflectionClass) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $classReflection; @@ -60,79 +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(), - $this->fetchedNodesResult->getLocatedSource(), - $functionNodes[$functionName]->getNamespace() - ); - if (!$functionReflection instanceof ReflectionFunction) { - throw new \PHPStan\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( - $reflector, - $stmtConst->getNode(), - $this->fetchedNodesResult->getLocatedSource(), - $stmtConst->getNamespace() - ); - if ($constantReflection === null) { - continue; + $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 (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); + + if ($positionInNode === null) { + throw new ShouldNotHappenException(); } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; + } + + $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, + $classNode->getNode(), + $classNode->getLocatedSource(), + $classNode->getNamespace(), + ); + + if (!$classReflection instanceof ReflectionClass) { + throw new ShouldNotHappenException(); } - return $constantReflection; + $reflections[] = $classReflection; } + } + } - foreach (array_keys($stmtConst->getNode()->consts) as $i) { - $constantReflection = $nodeToReflection->__invoke( + if ($identifierType->isFunction()) { + $functionNodes = $fetchedNodesResult->getFunctionNodes(); + + foreach ($functionNodes as $functionNodesArray) { + foreach ($functionNodesArray as $functionNode) { + $functionReflection = $nodeToReflection->__invoke( $reflector, - $stmtConst->getNode(), - $this->fetchedNodesResult->getLocatedSource(), - $stmtConst->getNamespace(), - $i + $functionNode->getNode(), + $functionNode->getLocatedSource(), + $functionNode->getNamespace(), ); - if ($constantReflection === null) { + + $reflections[] = $functionReflection; + } + } + } + + if ($identifierType->isConstant()) { + $constantNodes = $fetchedNodesResult->getConstantNodes(); + foreach ($constantNodes as $constantNodesArray) { + foreach ($constantNodesArray as $fetchedConstantNode) { + $constantNode = $fetchedConstantNode->getNode(); + + if ($constantNode instanceof Const_) { + foreach ($constantNode->consts as $constPosition => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + $constantReflection = $nodeToReflection->__invoke( + $reflector, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + $constPosition, + ); + if (!$constantReflection instanceof ReflectionConstant) { + throw new ShouldNotHappenException(); + } + + $reflections[] = $constantReflection; + } + continue; } + + $constantReflection = $nodeToReflection->__invoke( + $reflector, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + ); if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); - } - if ($constantReflection->getName() !== $identifier->getName()) { - continue; + throw new ShouldNotHappenException(); } - return $constantReflection; + $reflections[] = $constantReflection; } } - - return null; } - throw new \PHPStan\ShouldNotHappenException(); - } - - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array - { - return []; // todo + return $reflections; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php index baaad91822..bd857f7489 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php @@ -2,17 +2,16 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -class OptimizedSingleFileSourceLocatorRepository -{ +use function array_key_exists; - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorFactory $factory; +final class OptimizedSingleFileSourceLocatorRepository +{ /** @var array */ private array $locators = []; - public function __construct(OptimizedSingleFileSourceLocatorFactory $factory) + public function __construct(private OptimizedSingleFileSourceLocatorFactory $factory) { - $this->factory = $factory; } public function getOrCreate(string $fileName): OptimizedSingleFileSourceLocator diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php new file mode 100644 index 0000000000..84985abbce --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -0,0 +1,295 @@ + + * @see https://github.com/composer/composer/pull/10107 + */ +final class PhpFileCleaner +{ + + /** @var array */ + private array $typeConfig = []; + + private string $restPattern; + + private string $contents = ''; + + private int $len = 0; + + private int $index = 0; + + public function __construct() + { + 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', + ]; + } + + $this->restPattern = '{[^{}?"\'typeConfig)) . ']+}A'; + } + + public function clean(string $contents, int $maxMatches): string + { + $this->contents = $contents; + $this->len = strlen($contents); + $this->index = 0; + + $inType = false; + $typeLevel = 0; + + $inDefine = false; + + $clean = ''; + while ($this->index < $this->len) { + $this->skipToPhp(); + $clean .= 'index < $this->len) { + $char = $this->contents[$this->index]; + if ($char === '?' && $this->peek('>')) { + $clean .= '?>'; + $this->index += 2; + continue 2; + } + + if (in_array($char, ['"', "'"], true)) { + if ($inDefine) { + $clean .= $char . $this->consumeString($char); + $inDefine = false; + } else { + $this->skipString($char); + $clean .= 'null'; + } + + continue; + } + + 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; + } + + if ($char === '<' && $this->peek('<') && $this->match('{<<<[ \t]*+([\'"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+)\\1(?:\r\n|\n|\r)}A', $match)) { + $this->index += strlen($match[0]); + $this->skipHeredoc($match[2]); + $clean .= 'null'; + continue; + } + + if ($char === '/') { + if ($this->peek('/')) { + $this->skipToNewline(); + continue; + } + if ($this->peek('*')) { + $this->skipComment(); + continue; + } + } + + 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']) { + if ($maxMatches === 1 && $this->match($type['pattern'], $match, $this->index - 1)) { + return $clean . $match[0]; + } + + $inType = true; + } + } + + $this->index += 1; + if ($this->match($this->restPattern, $match)) { + $clean .= $char . $match[0]; + $this->index += strlen($match[0]); + } else { + $clean .= $char; + } + } + } + + return $clean; + } + + private function skipToPhp(): void + { + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '<' && $this->peek('?')) { + $this->index += 2; + break; + } + + $this->index += 1; + } + } + + private function 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; + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { + $this->index += 2; + continue; + } + if ($this->contents[$this->index] === $delimiter) { + $this->index += 1; + break; + } + $this->index += 1; + } + } + + private function skipComment(): void + { + $this->index += 2; + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '*' && $this->peek('/')) { + $this->index += 2; + break; + } + + $this->index += 1; + } + } + + private function skipToNewline(): void + { + while ($this->index < $this->len) { + if (in_array($this->contents[$this->index], ["\r", "\n"], true)) { + return; + } + $this->index += 1; + } + } + + private function skipHeredoc(string $delimiter): void + { + $firstDelimiterChar = $delimiter[0]; + $delimiterLength = strlen($delimiter); + $delimiterPattern = '{' . preg_quote($delimiter) . '(?![a-zA-Z0-9_\x80-\xff])}A'; + + while ($this->index < $this->len) { + // check if we find the delimiter after some spaces/tabs + switch ($this->contents[$this->index]) { + case "\t": + case ' ': + $this->index += 1; + continue 2; + case $firstDelimiterChar: + if ( + substr($this->contents, $this->index, $delimiterLength) === $delimiter + && $this->match($delimiterPattern) + ) { + $this->index += $delimiterLength; + return; + } + break; + } + + // skip the rest of the line + while ($this->index < $this->len) { + $this->skipToNewline(); + + // skip newlines + while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) { + $this->index += 1; + } + + break; + } + } + } + + private function peek(string $char): bool + { + return $this->index + 1 < $this->len && $this->contents[$this->index + 1] === $char; + } + + /** + * @param string[]|null $match + * @param-out string[] $match + */ + private function match(string $regex, ?array &$match = null, ?int $offset = null): bool + { + 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 new file mode 100644 index 0000000000..f2225f9b8f --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php @@ -0,0 +1,44 @@ +isClass()) { + if ($this->phpStormStubsSourceStubber->isPresentClass($identifier->getName()) === false) { + return null; + } + } + + if ($identifier->isFunction()) { + if ($this->phpStormStubsSourceStubber->isPresentFunction($identifier->getName()) === false) { + 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/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 4a7df14252..2b4f272646 100644 --- a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php @@ -2,20 +2,19 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use Roave\BetterReflection\Identifier\Identifier; -use Roave\BetterReflection\Identifier\IdentifierType; -use Roave\BetterReflection\Reflection\Reflection; -use Roave\BetterReflection\Reflector\Reflector; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - -class SkipClassAliasSourceLocator implements SourceLocator +use PHPStan\BetterReflection\Identifier\Identifier; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; +use PHPStan\BetterReflection\Reflector\Reflector; +use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use ReflectionClass; +use function class_exists; + +final class SkipClassAliasSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - public function __construct(SourceLocator $sourceLocator) + public function __construct(private SourceLocator $sourceLocator) { - $this->sourceLocator = $sourceLocator; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection @@ -26,7 +25,10 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return $this->sourceLocator->locateIdentifier($reflector, $identifier); } - $reflection = new \ReflectionClass($className); + $reflection = new ReflectionClass($className); + if ($reflection->getName() === 'ReturnTypeWillChange') { + return $this->sourceLocator->locateIdentifier($reflector, $identifier); + } if ($reflection->getFileName() === false) { return $this->sourceLocator->locateIdentifier($reflector, $identifier); } diff --git a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php new file mode 100644 index 0000000000..9ea23cd6a9 --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -0,0 +1,22 @@ +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 fbe01b804c..0000000000 --- a/src/Reflection/BrokerAwareExtension.php +++ /dev/null @@ -1,12 +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 b14f227dbe..6d0452caff 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -2,113 +2,28 @@ namespace PHPStan\Reflection; -use PHPStan\TrinaryLogic; -use PHPStan\Type\ConstantTypeHelper; +use PhpParser\Node\Expr; use PHPStan\Type\Type; -class ClassConstantReflection implements ConstantReflection +/** @api */ +interface ClassConstantReflection extends ClassMemberReflection, ConstantReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; + public function getValueExpr(): Expr; - private \ReflectionClassConstant $reflection; + public function isFinal(): bool; - private ?string $deprecatedDescription; + public function hasPhpDocType(): bool; - private bool $isDeprecated; + public function getPhpDocType(): ?Type; - private bool $isInternal; + public function hasNativeType(): bool; - public function __construct( - ClassReflection $declaringClass, - \ReflectionClassConstant $reflection, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal - ) - { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - } - - public function getName(): string - { - return $this->reflection->getName(); - } - - public function getFileName(): ?string - { - $fileName = $this->declaringClass->getFileName(); - if ($fileName === false) { - return null; - } - - return $fileName; - } + public function getNativeType(): ?Type; /** - * @return mixed + * @return list */ - public function getValue() - { - return $this->reflection->getValue(); - } - - public function getValueType(): Type - { - return ConstantTypeHelper::getTypeFromValue($this->getValue()); - } - - 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 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; - } + public function getAttributes(): array; } diff --git a/src/Reflection/ClassMemberAccessAnswerer.php b/src/Reflection/ClassMemberAccessAnswerer.php index b19f8021d0..9eeb979821 100644 --- a/src/Reflection/ClassMemberAccessAnswerer.php +++ b/src/Reflection/ClassMemberAccessAnswerer.php @@ -2,17 +2,28 @@ namespace PHPStan\Reflection; +/** @api */ 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/ClassMemberReflection.php b/src/Reflection/ClassMemberReflection.php index 1cd77f883e..da2274a063 100644 --- a/src/Reflection/ClassMemberReflection.php +++ b/src/Reflection/ClassMemberReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +/** @api */ interface ClassMemberReflection { diff --git a/src/Reflection/ClassNameHelper.php b/src/Reflection/ClassNameHelper.php new file mode 100644 index 0000000000..8fe817e91d --- /dev/null +++ b/src/Reflection/ClassNameHelper.php @@ -0,0 +1,17 @@ +|null */ private ?array $ancestors = null; @@ -78,56 +123,73 @@ class ClassReflection implements ReflectionWithFilename /** @var array */ private array $subclasses = []; - /** @var string|false|null */ - private $filename; + private string|false|null $filename = false; + + private string|false|null $reflectionDocComment = false; + + private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false; + + private false|ResolvedPhpDocBlock $traitContextResolvedPhpDocBlock = false; + + /** @var array|null */ + private ?array $cachedInterfaces = null; + + private ClassReflection|false|null $cachedParentClass = false; + + /** @var array|null */ + private ?array $typeAliases = null; + + /** @var array */ + private static array $resolvingTypeAliasImports = []; + + /** @var array */ + private array $hasMethodCache = []; + + /** @var array */ + private array $hasPropertyCache = []; /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\FileTypeMapper $fileTypeMapper - * @param \PHPStan\Reflection\PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions - * @param \PHPStan\Reflection\MethodsClassReflectionExtension[] $methodsClassReflectionExtensions - * @param string $displayName - * @param \ReflectionClass $reflection - * @param string|null $anonymousFilename - * @param ResolvedPhpDocBlock|null $stubPhpDocBlock - * @param string|null $extraCacheKey + * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions + * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions + * @param string[] $universalObjectCratesClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - FileTypeMapper $fileTypeMapper, - array $propertiesClassReflectionExtensions, - array $methodsClassReflectionExtensions, - string $displayName, - \ReflectionClass $reflection, - ?string $anonymousFilename, - ?TemplateTypeMap $resolvedTemplateTypeMap, - ?ResolvedPhpDocBlock $stubPhpDocBlock, - ?string $extraCacheKey = null + 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|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, ) { - $this->reflectionProvider = $reflectionProvider; - $this->fileTypeMapper = $fileTypeMapper; - $this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; - $this->displayName = $displayName; - $this->reflection = $reflection; - $this->anonymousFilename = $anonymousFilename; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - $this->stubPhpDocBlock = $stubPhpDocBlock; - $this->extraCacheKey = $extraCacheKey; } - public function getNativeReflection(): \ReflectionClass + public function getNativeReflection(): ReflectionClass|ReflectionEnum { return $this->reflection; } - /** - * @return string|false - */ - public function getFileName() + public function getFileName(): ?string { - if (isset($this->filename)) { + if (!is_bool($this->filename)) { return $this->filename; } @@ -136,39 +198,26 @@ public function getFileName() } $fileName = $this->reflection->getFileName(); if ($fileName === false) { - return $this->filename = false; + return $this->filename = null; } - if (!file_exists($fileName)) { - return $this->filename = false; + if (!is_file($fileName)) { + return $this->filename = null; } return $this->filename = $fileName; } - public function getFileNameWithPhpDocs(): ?string + public function getParentClass(): ?ClassReflection { - if ($this->stubPhpDocBlock !== null) { - return $this->stubPhpDocBlock->getFilename(); - } - - $filename = $this->getFileName(); - if ($filename === false) { - return null; + if (!is_bool($this->cachedParentClass)) { + return $this->cachedParentClass; } - return $filename; - } - - /** - * @return false|\PHPStan\Reflection\ClassReflection - */ - public function getParentClass() - { $parentClass = $this->reflection->getParentClass(); if ($parentClass === false) { - return false; + return $this->cachedParentClass = null; } $extendsTag = $this->getFirstExtendsTag(); @@ -179,7 +228,9 @@ public function getParentClass() if ($this->isGeneric()) { $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, - $this->getActiveTemplateTypeMap() + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } @@ -193,13 +244,18 @@ public function getParentClass() $parentReflection = $this->reflectionProvider->getClass($parentClass->getName()); if ($parentReflection->isGeneric()) { return $parentReflection->withTypes( - array_values($parentReflection->getTemplateTypeMap()->resolveToBounds()->getTypes()) + array_values($parentReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), ); } + $this->cachedParentClass = $parentReflection; + return $parentReflection; } + /** + * @return class-string + */ public function getName(): string { return $this->reflection->getName(); @@ -207,19 +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 function (Type $type): string { - return $type->describe(VerbosityLevel::typeOnly()); - }, $this->resolvedTemplateTypeMap->getTypes())) . '>'; + return $this->displayName . '<' . implode(',', $templateTypes) . '>'; } public function getCacheKey(): string @@ -232,9 +295,22 @@ public function getCacheKey(): string $cacheKey = $this->displayName; if ($this->resolvedTemplateTypeMap !== null) { - $cacheKey .= '<' . implode(',', array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::cache()); - }, $this->resolvedTemplateTypeMap->getTypes())) . '>'; + $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) { @@ -298,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(); @@ -324,41 +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 { - foreach ($this->propertiesClassReflectionExtensions as $extension) { + if (array_key_exists($propertyName, $this->hasPropertyCache)) { + return $this->hasPropertyCache[$propertyName]; + } + + if ($this->isEnum()) { + return $this->hasPropertyCache[$propertyName] = $this->hasNativeProperty($propertyName); + } + + 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; } @@ -367,76 +529,142 @@ public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): } if (!isset($this->methods[$key])) { - throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName); + 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); } 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 \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName); + throw new MissingMethodFromReflectionException($this->getName(), $methodName); } return $this->getPhpExtension()->getNativeMethod($this, $methodName); } - /** - * @return MethodReflection[] - */ - public function getNativeMethods(): array + public function hasConstructor(): bool { - $methods = []; - foreach ($this->reflection->getMethods() as $method) { - $methods[] = $this->getNativeMethod($method->getName()); - } - - return $methods; + return $this->findConstructor() !== null; } - public function hasConstructor(): bool + public function getConstructor(): ExtendedMethodReflection { - return $this->reflection->getConstructor() !== null; + $constructor = $this->findConstructor(); + if ($constructor === null) { + throw new ShouldNotHappenException(); + } + return $this->getNativeMethod($constructor->getName()); } - public function getConstructor(): MethodReflection + private function findConstructor(): ?ReflectionMethod { $constructor = $this->reflection->getConstructor(); if ($constructor === null) { - throw new \PHPStan\ShouldNotHappenException(); + return null; } - return $this->getNativeMethod($constructor->getName()); + + if ($this->phpVersion->supportsLegacyConstructor()) { + return $constructor; + } + + if (strtolower($constructor->getName()) !== '__construct') { + return null; + } + + return $constructor; } private function getPhpExtension(): PhpClassReflectionExtension { $extension = $this->methodsClassReflectionExtensions[0]; if (!$extension instanceof PhpClassReflectionExtension) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } 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); + } + $key = $propertyName; 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; @@ -444,7 +672,14 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco } if (!isset($this->properties[$key])) { - throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName); + 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); } return $this->properties[$key]; @@ -458,7 +693,7 @@ public function hasNativeProperty(string $propertyName): bool public function getNativeProperty(string $propertyName): PhpPropertyReflection { if (!$this->hasNativeProperty($propertyName)) { - throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName); + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } return $this->getPhpExtension()->getNativeProperty($this, $propertyName); @@ -479,191 +714,430 @@ public function isTrait(): bool return $this->reflection->isTrait(); } - public function isClass(): bool + /** + * @phpstan-assert-if-true ReflectionEnum $this->reflection + * @phpstan-assert-if-true ReflectionEnum $this->getNativeReflection() + */ + public function isEnum(): bool { - return !$this->isInterface() && !$this->isTrait(); + return $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum(); } - public function isAnonymous(): bool + /** + * @return 'Interface'|'Trait'|'Enum'|'Class' + */ + public function getClassTypeDescription(): string { - return $this->anonymousFilename !== null; + if ($this->isInterface()) { + return 'Interface'; + } elseif ($this->isTrait()) { + return 'Trait'; + } elseif ($this->isEnum()) { + return 'Enum'; + } + + return 'Class'; } - public function isSubclassOf(string $className): bool + public function isReadOnly(): bool { - if (isset($this->subclasses[$className])) { - return $this->subclasses[$className]; + return $this->reflection->isReadOnly(); + } + + public function isBackedEnum(): bool + { + if (!$this->reflection instanceof ReflectionEnum) { + return false; } - if (!$this->reflectionProvider->hasClass($className)) { - return $this->subclasses[$className] = false; + return $this->reflection->isBacked(); + } + + public function getBackedEnumType(): ?Type + { + if (!$this->reflection instanceof ReflectionEnum) { + return null; } - try { - return $this->subclasses[$className] = $this->reflection->isSubclassOf($className); - } catch (\ReflectionException $e) { - return $this->subclasses[$className] = false; + if (!$this->reflection->isBacked()) { + return null; } + + return TypehintHelper::decideTypeFromReflection($this->reflection->getBackingType()); } - /** - * @return \PHPStan\Reflection\ClassReflection[] - */ - public function getParents(): array + public function hasEnumCase(string $name): bool { - $parents = []; - $parent = $this->getParentClass(); - while ($parent !== false) { - $parents[] = $parent; - $parent = $parent->getParentClass(); + if (!$this->isEnum()) { + return false; } - return $parents; + return $this->reflection->hasCase($name); } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return array */ - public function getInterfaces(): array + public function getEnumCases(): array { - $interfaces = []; - - $parent = $this->getParentClass(); - if ($parent !== false) { - foreach ($parent->getInterfaces() as $interface) { - $interfaces[$interface->getName()] = $interface; - } + if (!$this->isEnum()) { + throw new ShouldNotHappenException(); } - if ($this->reflection->isInterface()) { - $implementsTags = $this->getExtendsTags(); - } else { - $implementsTags = $this->getImplementsTags(); + if ($this->enumCases !== null) { + return $this->enumCases; } - $interfaceNames = $this->reflection->getInterfaceNames(); - $genericInterfaces = []; - - foreach ($implementsTags as $implementsTag) { - $implementedType = $implementsTag->getType(); - - if (!$this->isValidAncestorType($implementedType, $interfaceNames)) { - continue; - } - - if ($this->isGeneric()) { - $implementedType = TemplateTypeHelper::resolveTemplateTypes( - $implementedType, - $this->getActiveTemplateTypeMap() - ); + $cases = []; + $initializerExprContext = InitializerExprContext::fromClassReflection($this); + foreach ($this->reflection->getCases() as $case) { + $valueType = null; + if ($case instanceof ReflectionEnumBackedCase) { + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext); } + $caseName = $case->getName(); + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); + $cases[$caseName] = new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); + } - if (!$implementedType instanceof GenericObjectType) { - continue; - } + return $this->enumCases = $cases; + } - $reflectionIface = $implementedType->getClassReflection(); - if ($reflectionIface === null) { - continue; - } + public function getEnumCase(string $name): EnumCaseReflection + { + if (!$this->hasEnumCase($name)) { + throw new ShouldNotHappenException(sprintf('Enum case %s::%s does not exist.', $this->getDisplayName(), $name)); + } - $genericInterfaces[] = $reflectionIface; + if (!$this->reflection instanceof ReflectionEnum) { + throw new ShouldNotHappenException(); } - foreach ($genericInterfaces as $genericInterface) { - $interfaces = array_merge($interfaces, $genericInterface->getInterfaces()); + if ($this->enumCases !== null && array_key_exists($name, $this->enumCases)) { + return $this->enumCases[$name]; } - foreach ($genericInterfaces as $genericInterface) { - $interfaces[$genericInterface->getName()] = $genericInterface; + $case = $this->reflection->getCase($name); + $valueType = null; + if ($case instanceof ReflectionEnumBackedCase) { + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this)); } - foreach ($interfaceNames as $interfaceName) { - if (isset($interfaces[$interfaceName])) { - continue; - } + $attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName())); - $interfaceReflection = $this->reflectionProvider->getClass($interfaceName); - if (!$interfaceReflection->isGeneric()) { - $interfaces[$interfaceName] = $interfaceReflection; - continue; - } + return new EnumCaseReflection($this, $case, $valueType, $attributes, $this->deprecationProvider); + } - $interfaces[$interfaceName] = $interfaceReflection->withTypes( - array_values($interfaceReflection->getTemplateTypeMap()->resolveToBounds()->getTypes()) - ); - } + public function isClass(): bool + { + return !$this->isInterface() && !$this->isTrait() && !$this->isEnum(); + } - return $interfaces; + public function isAnonymous(): bool + { + return $this->anonymousFilename !== null; } - /** - * @return \PHPStan\Reflection\ClassReflection[] - */ - public function getTraits(): array + public function is(string $className): bool { - return array_map(function (\ReflectionClass $trait): ClassReflection { - return $this->reflectionProvider->getClass($trait->getName()); - }, $this->getNativeReflection()->getTraits()); + return $this->getName() === $className || $this->isSubclassOf($className); } /** - * @return string[] + * @deprecated Use isSubclassOfClass instead. */ - public function getParentClassesNames(): array + public function isSubclassOf(string $className): bool { - $parentNames = []; - $currentClassReflection = $this; - while ($currentClassReflection->getParentClass() !== false) { - $parentNames[] = $currentClassReflection->getParentClass()->getName(); - $currentClassReflection = $currentClassReflection->getParentClass(); + if (!$this->reflectionProvider->hasClass($className)) { + return false; } - return $parentNames; + return $this->isSubclassOfClass($this->reflectionProvider->getClass($className)); } - public function hasConstant(string $name): bool + public function isSubclassOfClass(self $class): bool { - if (!$this->getNativeReflection()->hasConstant($name)) { - return false; + $cacheKey = $class->getCacheKey(); + if (isset($this->subclasses[$cacheKey])) { + return $this->subclasses[$cacheKey]; } - $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); - if ($reflectionConstant === false) { - return false; + if ($class->isFinalByKeyword() || $class->isAnonymous()) { + return $this->subclasses[$cacheKey] = false; } - return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); + try { + return $this->subclasses[$cacheKey] = $this->reflection->isSubclassOf($class->getName()); + } catch (ReflectionException) { + return $this->subclasses[$cacheKey] = false; + } } - public function getConstant(string $name): ConstantReflection + public function implementsInterface(string $className): bool { - if (!isset($this->constants[$name])) { - $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); - if ($reflectionConstant === false) { - throw new \PHPStan\Reflection\MissingConstantFromReflectionException($this->getName(), $name); - } - - $deprecatedDescription = null; - $isDeprecated = false; - $isInternal = false; - if ($reflectionConstant->getDocComment() !== false && $this->getFileName() !== false) { - $docComment = $reflectionConstant->getDocComment(); - $fileName = $this->getFileName(); - $className = $reflectionConstant->getDeclaringClass()->getName(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $className, null, null, $docComment); + try { + return $this->reflection->implementsInterface($className); + } catch (ReflectionException) { + return false; + } + } - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); - $isInternal = $resolvedPhpDoc->isInternal(); - } + /** + * @return list + */ + public function getParents(): array + { + $parents = []; + $parent = $this->getParentClass(); + while ($parent !== null) { + $parents[] = $parent; + $parent = $parent->getParentClass(); + } + + return $parents; + } + + /** + * @return array + */ + public function getInterfaces(): array + { + if ($this->cachedInterfaces !== null) { + return $this->cachedInterfaces; + } + + $interfaces = $this->getImmediateInterfaces(); + $immediateInterfaces = $interfaces; + $parent = $this->getParentClass(); + while ($parent !== null) { + foreach ($parent->getImmediateInterfaces() as $parentInterface) { + $interfaces[$parentInterface->getName()] = $parentInterface; + foreach ($this->collectInterfaces($parentInterface) as $parentInterfaceInterface) { + $interfaces[$parentInterfaceInterface->getName()] = $parentInterfaceInterface; + } + } + + $parent = $parent->getParentClass(); + } + + foreach ($immediateInterfaces as $immediateInterface) { + foreach ($this->collectInterfaces($immediateInterface) as $interfaceInterface) { + $interfaces[$interfaceInterface->getName()] = $interfaceInterface; + } + } + + $this->cachedInterfaces = $interfaces; + + return $interfaces; + } + + /** + * @return array + */ + private function collectInterfaces(ClassReflection $interface): array + { + $interfaces = []; + foreach ($interface->getImmediateInterfaces() as $immediateInterface) { + $interfaces[$immediateInterface->getName()] = $immediateInterface; + foreach ($this->collectInterfaces($immediateInterface) as $immediateInterfaceInterface) { + $interfaces[$immediateInterfaceInterface->getName()] = $immediateInterfaceInterface; + } + } + + return $interfaces; + } + + /** + * @return array + */ + public function getImmediateInterfaces(): array + { + $indirectInterfaceNames = []; + $parent = $this->getParentClass(); + while ($parent !== null) { + foreach ($parent->getNativeReflection()->getInterfaceNames() as $parentInterfaceName) { + $indirectInterfaceNames[] = $parentInterfaceName; + } + + $parent = $parent->getParentClass(); + } + + foreach ($this->getNativeReflection()->getInterfaces() as $interfaceInterface) { + foreach ($interfaceInterface->getInterfaceNames() as $interfaceInterfaceName) { + $indirectInterfaceNames[] = $interfaceInterfaceName; + } + } + + if ($this->reflection->isInterface()) { + $implementsTags = $this->getExtendsTags(); + } else { + $implementsTags = $this->getImplementsTags(); + } + + $immediateInterfaceNames = array_diff($this->getNativeReflection()->getInterfaceNames(), $indirectInterfaceNames); + $immediateInterfaces = []; + foreach ($immediateInterfaceNames as $immediateInterfaceName) { + if (!$this->reflectionProvider->hasClass($immediateInterfaceName)) { + continue; + } + + $immediateInterface = $this->reflectionProvider->getClass($immediateInterfaceName); + if (array_key_exists($immediateInterface->getName(), $implementsTags)) { + $implementsTag = $implementsTags[$immediateInterface->getName()]; + $implementedType = $implementsTag->getType(); + if ($this->isGeneric()) { + $implementedType = TemplateTypeHelper::resolveTemplateTypes( + $implementedType, + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), + true, + ); + } + + if ( + $implementedType instanceof GenericObjectType + && $implementedType->getClassReflection() !== null + ) { + $immediateInterfaces[$immediateInterface->getName()] = $implementedType->getClassReflection(); + continue; + } + } + + if ($immediateInterface->isGeneric()) { + $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface->withTypes( + array_values($immediateInterface->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), + ); + continue; + } + + $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface; + } + + return $immediateInterfaces; + } + + /** + * @return array + */ + public function getTraits(bool $recursive = false): array + { + $traits = []; + + if ($recursive) { + foreach ($this->collectTraits($this->getNativeReflection()) as $trait) { + $traits[$trait->getName()] = $trait; + } + } else { + $traits = $this->getNativeReflection()->getTraits(); + } + + $traits = array_map(fn (ReflectionClass $trait): ClassReflection => $this->reflectionProvider->getClass($trait->getName()), $traits); + + if ($recursive) { + $parentClass = $this->getNativeReflection()->getParentClass(); + + if ($parentClass !== false) { + return array_merge( + $traits, + $this->reflectionProvider->getClass($parentClass->getName())->getTraits(true), + ); + } + } + + return $traits; + } + + /** + * @return list + */ + public function getParentClassesNames(): array + { + $parentNames = []; + $parentClass = $this->getParentClass(); + while ($parentClass !== null) { + $parentNames[] = $parentClass->getName(); + $parentClass = $parentClass->getParentClass(); + } - $this->constants[$name] = new ClassConstantReflection( - $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName()), + return $parentNames; + } + + public function hasConstant(string $name): bool + { + if (!$this->getNativeReflection()->hasConstant($name)) { + return false; + } + + $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); + if ($reflectionConstant === false) { + return false; + } + + return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); + } + + public function getConstant(string $name): ClassConstantReflection + { + if (!isset($this->constants[$name])) { + $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); + if ($reflectionConstant === false) { + throw new MissingConstantFromReflectionException($this->getName(), $name); + } + + $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; + $resolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( + $declaringClass->getName(), + $name, + ); + if ($resolvedPhpDoc === null) { + $docComment = null; + if ($reflectionConstant->getDocComment() !== false) { + $docComment = $reflectionConstant->getDocComment(); + } + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForConstant( + $docComment, + $declaringClass, + $fileName, + $name, + ); + } + + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } + $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 RealClassClassConstantReflection( + $this->initializerExprTypeResolver, + $declaringClass, $reflectionConstant, + $nativeType, + $phpDocType, $deprecatedDescription, $isDeprecated, - $isInternal + $isInternal, + $isFinal, + $this->attributeReflectionFactory->fromNativeReflection($reflectionConstant->getAttributes(), InitializerExprContext::fromClass($declaringClass->getName(), $fileName)), ); } return $this->constants[$name]; @@ -675,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(); @@ -689,13 +1163,67 @@ private function getTraitNames(): array return $traitNames; } - public function getDeprecatedDescription(): ?string + /** + * @return array + */ + public function getTypeAliases(): array { - if ($this->deprecatedDescription === null && $this->isDeprecated()) { + if ($this->typeAliases === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); - if ($resolvedPhpDoc !== null && $resolvedPhpDoc->getDeprecatedTag() !== null) { - $this->deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); + if ($resolvedPhpDoc === null) { + return $this->typeAliases = []; + } + + $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags(); + $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags(); + + // prevent circular imports + if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { + throw new CircularTypeAliasDefinitionException(); } + + self::$resolvingTypeAliasImports[$this->getName()] = true; + + $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): ?TypeAlias { + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); + + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + return null; + } + + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + + try { + $typeAliases = $importedFromReflection->getTypeAliases(); + } catch (CircularTypeAliasDefinitionException) { + return TypeAlias::invalid(); + } + + if (!array_key_exists($importedAlias, $typeAliases)) { + return null; + } + + return $typeAliases[$importedAlias]; + }, $typeAliasImportTags); + + unset(self::$resolvingTypeAliasImports[$this->getName()]); + + $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); + + $this->typeAliases = array_filter( + array_merge($importedAliases, $localAliases), + static fn (?TypeAlias $typeAlias): bool => $typeAlias !== null, + ); + } + + return $this->typeAliases; + } + + public function getDeprecatedDescription(): ?string + { + if ($this->isDeprecated === null) { + $this->resolveDeprecation(); } return $this->deprecatedDescription; @@ -704,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(); @@ -728,20 +1279,145 @@ public function isInternal(): bool public function isFinal(): bool { + if ($this->isFinalByKeyword()) { + return true; + } + if ($this->isFinal === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); - $this->isFinal = $this->reflection->isFinal() - || ($resolvedPhpDoc !== null && $resolvedPhpDoc->isFinal()); + $this->isFinal = $resolvedPhpDoc !== null && $resolvedPhpDoc->isFinal(); } return $this->isFinal; } + 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->findAttributeFlags() !== null; + } + + private function findAttributeFlags(): ?int + { + if ($this->isInterface() || $this->isTrait() || $this->isEnum()) { + return null; + } + + $nativeAttributes = $this->reflection->getAttributes(Attribute::class); + if (count($nativeAttributes) === 1) { + 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 + { + $flags = $this->findAttributeFlags(); + if ($flags === null) { + throw new ShouldNotHappenException(); + } + + return $flags; + } + public function getTemplateTypeMap(): TemplateTypeMap { if ($this->templateTypeMap !== null) { @@ -754,12 +1430,9 @@ public function getTemplateTypeMap(): TemplateTypeMap return $this->templateTypeMap; } - $templateTypeMap = new TemplateTypeMap(array_map(function (TemplateTag $tag): Type { - return TemplateTypeFactory::fromTemplateTag( - TemplateTypeScope::createWithClass($this->getName()), - $tag - ); - }, $this->getTemplateTags())); + $templateTypeScope = TemplateTypeScope::createWithClass($this->getName()); + + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $this->getTemplateTags())); $this->templateTypeMap = $templateTypeMap; @@ -767,13 +1440,66 @@ public function getTemplateTypeMap(): TemplateTypeMap } public function getActiveTemplateTypeMap(): TemplateTypeMap + { + if ($this->activeTemplateTypeMap !== null) { + return $this->activeTemplateTypeMap; + } + $resolved = $this->resolvedTemplateTypeMap; + if ($resolved !== null) { + $templateTypeMap = $this->getTemplateTypeMap(); + return $this->activeTemplateTypeMap = $resolved->map(static function (string $name, Type $type) use ($templateTypeMap): Type { + if ($type instanceof ErrorType) { + $templateType = $templateTypeMap->getType($name); + if ($templateType !== null) { + return TemplateTypeHelper::resolveToDefaults($templateType); + } + } + + return $type; + }); + } + + return $this->activeTemplateTypeMap = $this->getTemplateTypeMap(); + } + + 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) { + if ($this->isEnum()) { + return $this->isGeneric = false; + } + $this->isGeneric = count($this->getTemplateTags()) > 0; } @@ -782,7 +1508,6 @@ public function isGeneric(): bool /** * @param array $types - * @return \PHPStan\Type\Generic\TemplateTypeMap */ public function typeMapFromList(array $types): TemplateTypeMap { @@ -794,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(); @@ -811,7 +1556,23 @@ public function typeMapToList(TemplateTypeMap $typeMap): array $list = []; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $list[] = $typeMap->getType($tag->getName()) ?? new ErrorType(); + $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; @@ -824,34 +1585,151 @@ 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->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->resolvedCallSiteVarianceMap, + $this->finalByKeywordOverride, ); } - private function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + /** + * @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, + ); + } + + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock { if ($this->stubPhpDocBlock !== null) { return $this->stubPhpDocBlock; } $fileName = $this->getFileName(); - if ($fileName === false) { + if (is_bool($this->reflectionDocComment)) { + $docComment = $this->reflection->getDocComment(); + $this->reflectionDocComment = $docComment !== false ? $docComment : null; + } + + if ($this->reflectionDocComment === null) { return null; } - $docComment = $this->reflection->getDocComment(); - if ($docComment === false) { + 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; + } + + if ($this->reflectionDocComment === null) { return null; } - return $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $docComment); + 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 @@ -863,8 +1741,8 @@ private function getFirstExtendsTag(): ?ExtendsTag return null; } - /** @return ExtendsTag[] */ - private function getExtendsTags(): array + /** @return array */ + public function getExtendsTags(): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); if ($resolvedPhpDoc === null) { @@ -874,8 +1752,8 @@ private function getExtendsTags(): array return $resolvedPhpDoc->getExtendsTags(); } - /** @return ImplementsTag[] */ - private function getImplementsTags(): array + /** @return array */ + public function getImplementsTags(): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); if ($resolvedPhpDoc === null) { @@ -886,7 +1764,7 @@ private function getImplementsTags(): array } /** @return array */ - private function getTemplateTags(): array + public function getTemplateTags(): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); if ($resolvedPhpDoc === null) { @@ -931,7 +1809,7 @@ public function getAncestors(): array } $parent = $this->getParentClass(); - if ($parent !== false) { + if ($parent !== null) { $addToAncestors($parent->getName(), $parent); foreach ($parent->getAncestors() as $name => $ancestor) { $addToAncestors($name, $ancestor); @@ -980,7 +1858,59 @@ 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 + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getPropertyTags(); + } + + /** + * @return array + */ + public function getMethodTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getMethodTags(); + } + + /** + * @return list */ public function getResolvedMixinTypes(): array { @@ -993,11 +1923,27 @@ public function getResolvedMixinTypes(): array $types[] = TemplateTypeHelper::resolveTemplateTypes( $mixinTag->getType(), - $this->getActiveTemplateTypeMap() + $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 9acc50ae61..1705f47461 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -2,41 +2,29 @@ namespace PHPStan\Reflection; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; -class ClassReflectionExtensionRegistry +final class ClassReflectionExtensionRegistry { - /** @var \PHPStan\Reflection\PropertiesClassReflectionExtension[] */ - private array $propertiesClassReflectionExtensions; - - /** @var \PHPStan\Reflection\MethodsClassReflectionExtension[] */ - private array $methodsClassReflectionExtensions; - /** - * @param \PHPStan\Broker\Broker $broker - * @param \PHPStan\Reflection\PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions - * @param \PHPStan\Reflection\MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions + * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions */ public function __construct( - Broker $broker, - array $propertiesClassReflectionExtensions, - array $methodsClassReflectionExtensions + 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); - } - $this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; } /** - * @return \PHPStan\Reflection\PropertiesClassReflectionExtension[] + * @return PropertiesClassReflectionExtension[] */ public function getPropertiesClassReflectionExtensions(): array { @@ -44,11 +32,29 @@ public function getPropertiesClassReflectionExtensions(): array } /** - * @return \PHPStan\Reflection\MethodsClassReflectionExtension[] + * @return MethodsClassReflectionExtension[] */ 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 a703fa4a34..4b31de502d 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -2,28 +2,21 @@ 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 { - private string $name; - - private Type $valueType; - - private ?string $fileName; - public function __construct( - string $name, - Type $valueType, - ?string $fileName + private string $name, + private Type $valueType, + private ?string $fileName, + private TrinaryLogic $isDeprecated, + private ?string $deprecatedDescription, ) { - $this->name = $name; - $this->valueType = $valueType; - $this->fileName = $fileName; } public function getName(): string @@ -43,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 ada86cebfa..ebae755849 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -2,12 +2,23 @@ namespace PHPStan\Reflection; -interface ConstantReflection extends ClassMemberReflection, GlobalConstantReflection +use PHPStan\TrinaryLogic; +use PHPStan\Type\Type; + +/** @api */ +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 ExtendedMethodReflection $reflection, + private array $variants, + private ?array $namedArgumentsVariants, + private ?Type $selfOutType, + ) + { + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + 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 getDocComment(): ?string + { + return $this->reflection->getDocComment(); + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->reflection->getPrototype(); + } + + 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(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->reflection->getDeprecatedDescription(); + } + + 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(); + } + + 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 new file mode 100644 index 0000000000..3bf6a6eb84 --- /dev/null +++ b/src/Reflection/Dummy/ChangedTypePropertyReflection.php @@ -0,0 +1,154 @@ +reflection->getName(); + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + 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 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; + } + + public function getWritableType(): Type + { + return $this->writableType; + } + + public function canChangeTypeAfterAssignment(): bool + { + return $this->reflection->canChangeTypeAfterAssignment(); + } + + public function isReadable(): bool + { + return $this->reflection->isReadable(); + } + + public function isWritable(): bool + { + return $this->reflection->isWritable(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->reflection->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->reflection->getDeprecatedDescription(); + } + + public function isInternal(): TrinaryLogic + { + return $this->reflection->isInternal(); + } + + 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 2137837e39..0000000000 --- a/src/Reflection/Dummy/DummyConstantReflection.php +++ /dev/null @@ -1,88 +0,0 @@ -name = $name; - } - - public function getDeclaringClass(): ClassReflection - { - $broker = Broker::getInstance(); - - return $broker->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 18b75b19f4..c48d6904ce 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -2,23 +2,23 @@ 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 { - private ClassReflection $declaringClass; - - public function __construct(ClassReflection $declaringClass) + public function __construct(private ClassReflection $declaringClass) { - $this->declaringClass = $declaringClass; } public function getDeclaringClass(): ClassReflection @@ -54,16 +54,29 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { return [ - new FunctionVariant( + new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, [], false, - new VoidType() + 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(); @@ -84,6 +97,11 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getThrowType(): ?Type { return null; @@ -99,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 bf12ffb38b..dced9b6206 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -2,29 +2,29 @@ namespace PHPStan\Reflection\Dummy; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +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 { - private string $name; - - public function __construct(string $name) + public function __construct(private string $name) { - $this->name = $name; } public function getDeclaringClass(): ClassReflection { - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $broker->getClass(\stdClass::class); + return $reflectionProvider->getClass(stdClass::class); } public function isStatic(): bool @@ -52,9 +52,6 @@ public function getPrototype(): ClassMemberReflection return $this; } - /** - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getVariants(): array { return [ @@ -62,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(); @@ -77,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; @@ -97,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 1f5b97379e..ca828a53fe 100644 --- a/src/Reflection/Dummy/DummyPropertyReflection.php +++ b/src/Reflection/Dummy/DummyPropertyReflection.php @@ -2,21 +2,33 @@ namespace PHPStan\Reflection\Dummy; -use PHPStan\Broker\Broker; 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 { - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $broker->getClass(\stdClass::class); + return $reflectionProvider->getClass(stdClass::class); } public function isStatic(): bool @@ -34,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(); @@ -79,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 new file mode 100644 index 0000000000..7a86e38726 --- /dev/null +++ b/src/Reflection/EnumCaseReflection.php @@ -0,0 +1,81 @@ + $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 + { + return $this->declaringEnum; + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + 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 27ef1174ac..297e4dd7d3 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -5,26 +5,61 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +/** @api */ interface FunctionReflection { public function getName(): string; + public function getFileName(): ?string; + /** - * @return \PHPStan\Reflection\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; 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 6e89f2f31b..993bf34b3b 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -10,20 +11,14 @@ interface FunctionReflectionFactory { /** - * @param \ReflectionFunction $reflection - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|false $filename - * @return PhpFunctionReflection + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes + * @param list $attributes */ public function create( - \ReflectionFunction $reflection, + ReflectionFunction $reflection, TemplateTypeMap $templateTypeMap, array $phpDocParameterTypes, ?Type $phpDocReturnType, @@ -31,8 +26,15 @@ public function create( ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, - bool $isFinal, - $filename + ?string $filename, + ?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 0475d63d65..7c69274ef0 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -3,40 +3,31 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; +/** + * @api + */ class FunctionVariant implements ParametersAcceptor { - private TemplateTypeMap $templateTypeMap; - - private ?TemplateTypeMap $resolvedTemplateTypeMap; - - /** @var array */ - private array $parameters; - - private bool $isVariadic; - - private Type $returnType; + private TemplateTypeVarianceMap $callSiteVarianceMap; /** - * @param array $parameters - * @param bool $isVariadic - * @param Type $returnType + * @api + * @param list $parameters */ public function __construct( - TemplateTypeMap $templateTypeMap, - ?TemplateTypeMap $resolvedTemplateTypeMap, - array $parameters, - bool $isVariadic, - Type $returnType + private TemplateTypeMap $templateTypeMap, + private ?TemplateTypeMap $resolvedTemplateTypeMap, + private array $parameters, + private bool $isVariadic, + private Type $returnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, ) { - $this->templateTypeMap = $templateTypeMap; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - $this->parameters = $parameters; - $this->isVariadic = $isVariadic; - $this->returnType = $returnType; + $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); } public function getTemplateTypeMap(): TemplateTypeMap @@ -49,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 73e0d6f1e1..0000000000 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ /dev/null @@ -1,65 +0,0 @@ - $parameters - * @param bool $isVariadic - * @param Type $returnType - * @param Type $phpDocReturnType - * @param Type $nativeReturnType - */ - public function __construct( - TemplateTypeMap $templateTypeMap, - ?TemplateTypeMap $resolvedTemplateTypeMap, - array $parameters, - bool $isVariadic, - Type $returnType, - Type $phpDocReturnType, - Type $nativeReturnType - ) - { - parent::__construct( - $templateTypeMap, - $resolvedTemplateTypeMap, - $parameters, - $isVariadic, - $returnType - ); - $this->phpDocReturnType = $phpDocReturnType; - $this->nativeReturnType = $nativeReturnType; - } - - /** - * @return array - */ - public function getParameters(): array - { - /** @var \PHPStan\Reflection\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/Generic/ResolvedFunctionVariant.php b/src/Reflection/Generic/ResolvedFunctionVariant.php deleted file mode 100644 index f839d8ed1d..0000000000 --- a/src/Reflection/Generic/ResolvedFunctionVariant.php +++ /dev/null @@ -1,86 +0,0 @@ -parametersAcceptor = $parametersAcceptor; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map(function (ParameterReflection $param): ParameterReflection { - return new DummyParameter( - $param->getName(), - TemplateTypeHelper::resolveTemplateTypes($param->getType(), $this->resolvedTemplateTypeMap), - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue() - ); - }, $this->parametersAcceptor->getParameters()); - - $this->parameters = $parameters; - } - - return $parameters; - } - - public function isVariadic(): bool - { - return $this->parametersAcceptor->isVariadic(); - } - - public function getReturnType(): Type - { - $type = $this->returnType; - - if ($type === null) { - $type = TemplateTypeHelper::resolveTemplateTypes( - $this->parametersAcceptor->getReturnType(), - $this->resolvedTemplateTypeMap - ); - - $this->returnType = $type; - } - - return $type; - } - -} diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index b44a8f4dc0..35f70bf37d 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -2,45 +2,136 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Generic\ResolvedFunctionVariant; +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 { /** - * Resolve template types - * - * @param \PHPStan\Type\Type[] $argTypes Unpacked arguments + * @api + * @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 function (string $name, Type $type): Type { - return 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 ad6d9d967e..0000000000 --- a/src/Reflection/GlobalConstantReflection.php +++ /dev/null @@ -1,23 +0,0 @@ -methodReflection = $methodReflection; } - public function getMethod(): MethodReflection + public function getMethod(): ExtendedMethodReflection { return $this->methodReflection; } @@ -31,9 +32,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - /** - * @return array - */ + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; @@ -49,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 cb2ecabc58..c92c6c5b74 100644 --- a/src/Reflection/MethodPrototypeReflection.php +++ b/src/Reflection/MethodPrototypeReflection.php @@ -2,55 +2,27 @@ namespace PHPStan\Reflection; -class MethodPrototypeReflection implements ClassMemberReflection -{ - - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private string $name; - - private bool $isStatic; - - private bool $isPrivate; - - private bool $isPublic; - - private bool $isAbstract; - - private bool $isFinal; +use PHPStan\Type\Type; - /** @var ParametersAcceptor[] */ - private array $variants; +final class MethodPrototypeReflection implements ClassMemberReflection +{ /** - * @param string $name - * @param ClassReflection $declaringClass - * @param bool $isStatic - * @param bool $isPrivate - * @param bool $isPublic - * @param bool $isAbstract - * @param bool $isFinal * @param ParametersAcceptor[] $variants */ public function __construct( - string $name, - ClassReflection $declaringClass, - bool $isStatic, - bool $isPrivate, - bool $isPublic, - bool $isAbstract, - bool $isFinal, - array $variants + private string $name, + private ClassReflection $declaringClass, + private bool $isStatic, + private bool $isPrivate, + private bool $isPublic, + private bool $isAbstract, + private bool $isFinal, + private bool $isInternal, + private array $variants, + private ?Type $tentativeReturnType, ) { - $this->name = $name; - $this->declaringClass = $declaringClass; - $this->isStatic = $isStatic; - $this->isPrivate = $isPrivate; - $this->isPublic = $isPublic; - $this->isAbstract = $isAbstract; - $this->isFinal = $isFinal; - $this->variants = $variants; } public function getName(): string @@ -88,6 +60,11 @@ public function isFinal(): bool return $this->isFinal; } + public function isInternal(): bool + { + return $this->isInternal; + } + public function getDocComment(): ?string { return null; @@ -101,4 +78,9 @@ public function getVariants(): array return $this->variants; } + public function getTentativeReturnType(): ?Type + { + return $this->tentativeReturnType; + } + } diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index a65c2016d7..529a5011dd 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -5,6 +5,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +/** @api */ interface MethodReflection extends ClassMemberReflection { @@ -13,7 +14,7 @@ public function getName(): string; public function getPrototype(): ClassMemberReflection; /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return list */ public function getVariants(): array; diff --git a/src/Reflection/MethodsClassReflectionExtension.php b/src/Reflection/MethodsClassReflectionExtension.php index fa602ce2d7..5817ac7657 100644 --- a/src/Reflection/MethodsClassReflectionExtension.php +++ b/src/Reflection/MethodsClassReflectionExtension.php @@ -2,6 +2,23 @@ namespace PHPStan\Reflection; +/** + * 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 7535b78fa7..e57d3c3f4f 100644 --- a/src/Reflection/MissingConstantFromReflectionException.php +++ b/src/Reflection/MissingConstantFromReflectionException.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection; -class MissingConstantFromReflectionException extends \Exception +use Exception; +use function sprintf; + +final class MissingConstantFromReflectionException extends Exception { public function __construct( string $className, - string $constantName + string $constantName, ) { parent::__construct( sprintf( 'Constant %s was not found in reflection of class %s.', $constantName, - $className - ) + $className, + ), ); } diff --git a/src/Reflection/MissingMethodFromReflectionException.php b/src/Reflection/MissingMethodFromReflectionException.php index 30f725c49a..48051aafc1 100644 --- a/src/Reflection/MissingMethodFromReflectionException.php +++ b/src/Reflection/MissingMethodFromReflectionException.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection; -class MissingMethodFromReflectionException extends \Exception +use Exception; +use function sprintf; + +final class MissingMethodFromReflectionException extends Exception { public function __construct( string $className, - string $methodName + string $methodName, ) { parent::__construct( sprintf( 'Method %s() was not found in reflection of class %s.', $methodName, - $className - ) + $className, + ), ); } diff --git a/src/Reflection/MissingPropertyFromReflectionException.php b/src/Reflection/MissingPropertyFromReflectionException.php index af67614d52..2e64aee94c 100644 --- a/src/Reflection/MissingPropertyFromReflectionException.php +++ b/src/Reflection/MissingPropertyFromReflectionException.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection; -class MissingPropertyFromReflectionException extends \Exception +use Exception; +use function sprintf; + +final class MissingPropertyFromReflectionException extends Exception { public function __construct( string $className, - string $propertyName + string $propertyName, ) { parent::__construct( sprintf( 'Property $%s was not found in reflection of class %s.', $propertyName, - $className - ) + $className, + ), ); } diff --git a/src/Reflection/Mixin/MixinMethodReflection.php b/src/Reflection/Mixin/MixinMethodReflection.php new file mode 100644 index 0000000000..15fc4d8ad8 --- /dev/null +++ b/src/Reflection/Mixin/MixinMethodReflection.php @@ -0,0 +1,88 @@ +reflection->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->static; + } + + public function isPrivate(): bool + { + return $this->reflection->isPrivate(); + } + + public function isPublic(): bool + { + return $this->reflection->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->reflection->getDocComment(); + } + + public function getName(): string + { + return $this->reflection->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->reflection->getPrototype(); + } + + public function getVariants(): array + { + return $this->reflection->getVariants(); + } + + public function isDeprecated(): TrinaryLogic + { + return $this->reflection->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->reflection->getDeprecatedDescription(); + } + + public function isFinal(): TrinaryLogic + { + return $this->reflection->isFinal(); + } + + public function isInternal(): TrinaryLogic + { + return $this->reflection->isInternal(); + } + + public function getThrowType(): ?Type + { + return $this->reflection->getThrowType(); + } + + public function hasSideEffects(): TrinaryLogic + { + return $this->reflection->hasSideEffects(); + } + +} diff --git a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php index 43abed6e2c..58ae58ca2f 100644 --- a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php @@ -6,20 +6,22 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; -use PHPStan\Type\TypeUtils; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\VerbosityLevel; +use function array_intersect; +use function count; -class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension +final class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var string[] */ - private array $mixinExcludeClasses; + /** @var array> */ + private array $inProcess = []; /** * @param string[] $mixinExcludeClasses */ - public function __construct(array $mixinExcludeClasses) + public function __construct(private array $mixinExcludeClasses) { - $this->mixinExcludeClasses = $mixinExcludeClasses; } public function hasMethod(ClassReflection $classReflection, string $methodName): bool @@ -31,7 +33,7 @@ public function getMethod(ClassReflection $classReflection, string $methodName): { $method = $this->findMethod($classReflection, $methodName); if ($method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $method; @@ -41,24 +43,54 @@ 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; } + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + if (isset($this->inProcess[$typeDescription][$methodName])) { + continue; + } + + $this->inProcess[$typeDescription][$methodName] = true; + if (!$type->hasMethod($methodName)->yes()) { + unset($this->inProcess[$typeDescription][$methodName]); + continue; + } + + $method = $type->getMethod($methodName, new OutOfClassScope()); + + unset($this->inProcess[$typeDescription][$methodName]); + + $static = $method->isStatic(); + if ( + !$static + && $classReflection->hasNativeMethod('__callStatic') + ) { + $static = true; + } + + return new MixinMethodReflection($method, $static); + } + + foreach ($classReflection->getTraits() as $traitClass) { + $methodWithDeclaringClass = $this->findMethod($traitClass, $methodName); + if ($methodWithDeclaringClass === null) { continue; } - return $type->getMethod($methodName, new OutOfClassScope()); + return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $method = $this->findMethod($parentClass, $methodName); - if ($method === null) { - continue; + if ($method !== null) { + return $method; } - return $method; + $parentClass = $parentClass->getParentClass(); } return null; diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 4ab4635111..4b21f92451 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -6,20 +6,22 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; -use PHPStan\Type\TypeUtils; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\VerbosityLevel; +use function array_intersect; +use function count; -class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { - /** @var string[] */ - private array $mixinExcludeClasses; + /** @var array> */ + private array $inProcess = []; /** * @param string[] $mixinExcludeClasses */ - public function __construct(array $mixinExcludeClasses) + public function __construct(private array $mixinExcludeClasses) { - $this->mixinExcludeClasses = $mixinExcludeClasses; } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -31,7 +33,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa { $property = $this->findProperty($classReflection, $propertyName); if ($property === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $property; @@ -41,24 +43,45 @@ 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; } + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + if (isset($this->inProcess[$typeDescription][$propertyName])) { + continue; + } + + $this->inProcess[$typeDescription][$propertyName] = true; + if (!$type->hasProperty($propertyName)->yes()) { + unset($this->inProcess[$typeDescription][$propertyName]); continue; } - return $type->getProperty($propertyName, new OutOfClassScope()); + $property = $type->getProperty($propertyName, new OutOfClassScope()); + unset($this->inProcess[$typeDescription][$propertyName]); + + return $property; } - foreach ($classReflection->getParents() as $parentClass) { - $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 b81c1451b4..7668d51f9e 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -2,38 +2,43 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\AttributeReflection; +use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use function count; -class NativeFunctionReflection implements \PHPStan\Reflection\FunctionReflection +final class NativeFunctionReflection implements FunctionReflection { - private string $name; + private Assertions $assertions; - /** @var \PHPStan\Reflection\ParametersAcceptor[] */ - private array $variants; - - private ?\PHPStan\Type\Type $throwType; - - private TrinaryLogic $hasSideEffects; + private TrinaryLogic $returnsByReference; /** - * @param string $name - * @param \PHPStan\Reflection\ParametersAcceptor[] $variants - * @param \PHPStan\Type\Type|null $throwType - * @param \PHPStan\TrinaryLogic $hasSideEffects + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( - string $name, - array $variants, - ?Type $throwType, - TrinaryLogic $hasSideEffects + 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->name = $name; - $this->variants = $variants; - $this->throwType = $throwType; - $this->hasSideEffects = $hasSideEffects; + $this->assertions = $assertions ?? Assertions::createEmpty(); + $this->returnsByReference = $returnsByReference ?? TrinaryLogic::createMaybe(); } public function getName(): string @@ -41,14 +46,31 @@ public function getName(): string return $this->name; } - /** - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ + public function getFileName(): ?string + { + return null; + } + 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; @@ -61,7 +83,7 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createFromBoolean($this->isDeprecated); } public function isInternal(): TrinaryLogic @@ -69,14 +91,63 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isFinal(): TrinaryLogic + public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::createNo(); + if ($this->isVoid()) { + return TrinaryLogic::createYes(); + } + + return $this->hasSideEffects; } - public function hasSideEffects(): TrinaryLogic + public function isPure(): TrinaryLogic { - return $this->hasSideEffects; + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + + private function isVoid(): bool + { + foreach ($this->variants as $variant) { + if (!$variant->getReturnType()->isVoid()->yes()) { + return false; + } + } + + return true; + } + + 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 faa623ff6c..34ec4e3e51 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -2,54 +2,46 @@ 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\Php\BuiltinMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use PHPStan\Type\TypehintHelper; +use ReflectionException; +use function count; +use function strtolower; -class NativeMethodReflection implements MethodReflection +final class NativeMethodReflection implements ExtendedMethodReflection { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private BuiltinMethodReflection $reflection; - - /** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] */ - private array $variants; - - private TrinaryLogic $hasSideEffects; - - private ?string $stubPhpDocString; - /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Reflection\ClassReflection $declaringClass - * @param BuiltinMethodReflection $reflection - * @param \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] $variants - * @param TrinaryLogic $hasSideEffects - * @param string|null $stubPhpDocString + * @param list $variants + * @param list|null $namedArgumentsVariants + * @param list $attributes */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassReflection $declaringClass, - BuiltinMethodReflection $reflection, - array $variants, - TrinaryLogic $hasSideEffects, - ?string $stubPhpDocString + private ReflectionProvider $reflectionProvider, + private ClassReflection $declaringClass, + 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, ) { - $this->reflectionProvider = $reflectionProvider; - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->variants = $variants; - $this->hasSideEffects = $hasSideEffects; - $this->stubPhpDocString = $stubPhpDocString; } public function getDeclaringClass(): ClassReflection @@ -72,16 +64,28 @@ 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(), null, $prototypeDeclaringClass); + } return new MethodPrototypeReflection( $prototypeMethod->getName(), @@ -91,9 +95,11 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), - $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants() + $prototypeMethod->isInternal(), + $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), + $tentativeReturnType, ); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return $this; } } @@ -103,14 +109,26 @@ public function getName(): string return $this->reflection->getName(); } - /** - * @return \PHPStan\Reflection\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; @@ -118,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 @@ -126,28 +144,88 @@ 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 null; + return $this->throwType; } public function hasSideEffects(): TrinaryLogic { + $name = strtolower($this->getName()); + $isVoid = $this->isVoid(); + if ( + $name !== '__construct' + && $isVoid + ) { + return TrinaryLogic::createYes(); + } + return $this->hasSideEffects; } - public function getDocComment(): ?string + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + + private function isVoid(): bool { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; + foreach ($this->variants as $variant) { + if (!$variant->getReturnType()->isVoid()->yes()) { + return false; + } } - return $this->reflection->getDocComment(); + return true; + } + + public function getDocComment(): ?string + { + 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 3d15d91052..e812086830 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -5,37 +5,20 @@ 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 { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - - private ?\PHPStan\Type\Type $defaultValue; - public function __construct( - string $name, - bool $optional, - Type $type, - PassedByReference $passedByReference, - bool $variadic, - ?Type $defaultValue + private string $name, + private bool $optional, + private Type $type, + private PassedByReference $passedByReference, + private bool $variadic, + private ?Type $defaultValue, ) { - $this->name = $name; - $this->optional = $optional; - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; - $this->defaultValue = $defaultValue; } public function getName(): string @@ -68,19 +51,15 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } - /** - * @param mixed[] $properties - * @return self - */ - 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 f9f092fb4b..0000000000 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ /dev/null @@ -1,107 +0,0 @@ -name = $name; - $this->optional = $optional; - $this->type = $type; - $this->phpDocType = $phpDocType; - $this->nativeType = $nativeType; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; - $this->defaultValue = $defaultValue; - } - - public function getName(): string - { - 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 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 - * @return self - */ - 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/ObjectTypeMethodReflection.php b/src/Reflection/ObjectTypeMethodReflection.php deleted file mode 100644 index 5de568fe8a..0000000000 --- a/src/Reflection/ObjectTypeMethodReflection.php +++ /dev/null @@ -1,143 +0,0 @@ -objectType = $objectType; - $this->reflection = $reflection; - } - - public function getDeclaringClass(): ClassReflection - { - return $this->reflection->getDeclaringClass(); - } - - 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 getDocComment(): ?string - { - return $this->reflection->getDocComment(); - } - - public function getName(): string - { - return $this->reflection->getName(); - } - - public function getPrototype(): ClassMemberReflection - { - return $this->reflection->getPrototype(); - } - - /** - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ - public function getVariants(): array - { - if ($this->variants !== null) { - return $this->variants; - } - - $variants = []; - foreach ($this->reflection->getVariants() as $variant) { - $variants[] = $this->processVariant($variant); - } - - $this->variants = $variants; - - return $this->variants; - } - - private function processVariant(ParametersAcceptor $acceptor): ParametersAcceptor - { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - array_map(function (ParameterReflection $parameter): ParameterReflection { - $type = TypeTraverser::map($parameter->getType(), function (Type $type, callable $traverse): Type { - if ($type instanceof StaticType) { - return $traverse($this->objectType); - } - - return $traverse($type); - }); - - return new DummyParameter( - $parameter->getName(), - $type, - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue() - ); - }, $acceptor->getParameters()), - $acceptor->isVariadic(), - $acceptor->getReturnType() - ); - } - - public function isDeprecated(): TrinaryLogic - { - return $this->reflection->isDeprecated(); - } - - public function getDeprecatedDescription(): ?string - { - return $this->reflection->getDeprecatedDescription(); - } - - public function isFinal(): TrinaryLogic - { - return $this->reflection->isFinal(); - } - - public function isInternal(): TrinaryLogic - { - return $this->reflection->isInternal(); - } - - public function getThrowType(): ?Type - { - return $this->reflection->getThrowType(); - } - - public function hasSideEffects(): TrinaryLogic - { - return $this->reflection->hasSideEffects(); - } - -} 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/ParameterReflection.php b/src/Reflection/ParameterReflection.php index 7f2b150bfe..8efbfa7c06 100644 --- a/src/Reflection/ParameterReflection.php +++ b/src/Reflection/ParameterReflection.php @@ -4,6 +4,7 @@ use PHPStan\Type\Type; +/** @api */ interface ParameterReflection { 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 f335deb79d..81bc3a2a99 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -2,69 +2,474 @@ namespace PHPStan\Reflection; +use Closure; +use PhpParser\Node; +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; -class ParametersAcceptorSelector +/** + * @api + */ +final class ParametersAcceptorSelector { /** - * @template T of ParametersAcceptor - * @param T[] $parametersAcceptors - * @return T - */ - public static function selectSingle( - array $parametersAcceptors - ): ParametersAcceptor - { - if (count($parametersAcceptors) !== 1) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $parametersAcceptors[0]; - } - - /** - * @param Scope $scope - * @param \PhpParser\Node\Arg[] $args + * @param Node\Arg[] $args * @param ParametersAcceptor[] $parametersAcceptors - * @return ParametersAcceptor + * @param ParametersAcceptor[]|null $namedArgumentsVariants */ public static function selectFromArgs( Scope $scope, array $args, - array $parametersAcceptors + array $parametersAcceptors, + ?array $namedArgumentsVariants = null, ): ParametersAcceptor { $types = []; $unpack = false; - foreach ($args as $arg) { - $type = $scope->getType($arg->value); - if ($arg->unpack) { + if ( + count($args) > 0 + && count($parametersAcceptors) > 0 + ) { + $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + if ($arrayMapArgs !== null) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $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); + } + } + } + } else { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null); + } + } + $parameters[0] = new NativeParameterReflection( + $parameters[0]->getName(), + $parameters[0]->isOptional(), + new UnionType([ + new CallableType($callbackParameters, new MixedType(), false), + new NullType(), + ]), + $parameters[0]->passedByReference(), + $parameters[0]->isVariadic(), + $parameters[0]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + 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->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ]; + } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { + $arrayFilterParameters = [ + 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), + ]; + } + } + } + + $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( + [ + new DummyParameter('value', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($argType), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + 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])) { + $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]; + } + + $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 \PHPStan\Type\Type[] $types + * @param array $types * @param ParametersAcceptor[] $parametersAcceptors - * @param bool $unpack - * @return ParametersAcceptor */ public static function selectFromTypes( array $types, array $parametersAcceptors, - bool $unpack + bool $unpack, ): ParametersAcceptor { if (count($parametersAcceptors) === 1) { @@ -72,8 +477,8 @@ public static function selectFromTypes( } if (count($parametersAcceptors) === 0) { - throw new \PHPStan\ShouldNotHappenException( - 'getVariants() must return at least one variant.' + throw new ShouldNotHappenException( + 'getVariants() must return at least one variant.', ); } @@ -129,7 +534,7 @@ public static function selectFromTypes( break; } - $type = $types[count($types) - 1]; + $type = $types[array_key_last($types)]; } else { $type = $types[$i]; } @@ -137,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); } } @@ -163,22 +568,21 @@ 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 - * @return ParametersAcceptor */ - public static function combineAcceptors(array $acceptors): ParametersAcceptor + public static function combineAcceptors(array $acceptors): ExtendedParametersAcceptor { if (count($acceptors) === 0) { - throw new \PHPStan\ShouldNotHappenException( - 'getVariants() must return at least one variant.' + throw new ShouldNotHappenException( + 'getVariants() must return at least one variant.', ); } if (count($acceptors) === 1) { - return $acceptors[0]; + return self::wrapAcceptor($acceptors[0]); } $minimumNumberOfParameters = null; @@ -201,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->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; } @@ -233,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 + $defaultValue, + $nativeType, + $phpDocType, + $outType, + $immediatelyInvokedCallable, + $closureThisType, + $attributes, ); if ($isVariadic) { @@ -249,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 + $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 fbc22be4ea..0000000000 --- a/src/Reflection/ParametersAcceptorWithPhpDocs.php +++ /dev/null @@ -1,19 +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 17e233c124..804d049b43 100644 --- a/src/Reflection/PassedByReference.php +++ b/src/Reflection/PassedByReference.php @@ -2,7 +2,12 @@ namespace PHPStan\Reflection; -class PassedByReference +use function array_key_exists; + +/** + * @api + */ +final class PassedByReference { private const NO = 1; @@ -12,11 +17,8 @@ class PassedByReference /** @var self[] */ private static array $registry = []; - private int $value; - - private function __construct(int $value) + private function __construct(private int $value) { - $this->value = $value; } private static function create(int $value): self @@ -74,13 +76,4 @@ public function combine(self $other): self return $this; } - /** - * @param mixed[] $properties - * @return self - */ - 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 efc2f4e0b4..0000000000 --- a/src/Reflection/Php/BuiltinMethodReflection.php +++ /dev/null @@ -1,58 +0,0 @@ -nativeMethodReflection = $nativeMethodReflection; - $this->closureType = $closureType; } public function getDeclaringClass(): ClassReflection @@ -64,9 +66,6 @@ public function getPrototype(): ClassMemberReflection return $this->nativeMethodReflection->getPrototype(); } - /** - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getVariants(): array { $parameters = $this->closureType->getParameters(); @@ -76,22 +75,48 @@ public function getVariants(): array new ObjectWithoutClassType(), PassedByReference::createNo(), false, - null + null, ); 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(), + $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(); @@ -107,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(); @@ -122,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 new file mode 100644 index 0000000000..abeb693630 --- /dev/null +++ b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php @@ -0,0 +1,37 @@ +prototype->doNotResolveTemplateTypeMapToBounds(), $this->closure); + } + + public function getNakedMethod(): ExtendedMethodReflection + { + return $this->getTransformedMethod(); + } + + public function getTransformedMethod(): ExtendedMethodReflection + { + return new ClosureCallMethodReflection($this->prototype->getTransformedMethod(), $this->closure); + } + + public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection + { + return new self($this->prototype->withCalledOnType($type), $this->closure); + } + +} diff --git a/src/Reflection/Php/DummyParameter.php b/src/Reflection/Php/DummyParameter.php index 1420d4814f..5d73990a42 100644 --- a/src/Reflection/Php/DummyParameter.php +++ b/src/Reflection/Php/DummyParameter.php @@ -9,27 +9,11 @@ class DummyParameter implements ParameterReflection { - private string $name; + private PassedByReference $passedByReference; - private \PHPStan\Type\Type $type; - - private bool $optional; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - - /** @var ?\PHPStan\Type\Type */ - private ?\PHPStan\Type\Type $defaultValue; - - public function __construct(string $name, Type $type, bool $optional, ?PassedByReference $passedByReference, bool $variadic, ?Type $defaultValue) + public function __construct(private string $name, private Type $type, private bool $optional, ?PassedByReference $passedByReference, private bool $variadic, private ?Type $defaultValue) { - $this->name = $name; - $this->type = $type; - $this->optional = $optional; $this->passedByReference = $passedByReference ?? PassedByReference::createNo(); - $this->variadic = $variadic; - $this->defaultValue = $defaultValue; } public function getName(): string diff --git a/src/Reflection/Php/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 new file mode 100644 index 0000000000..ecf72e435d --- /dev/null +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -0,0 +1,164 @@ +declaringClass; + } + + public function isStatic(): bool + { + return true; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getName(): string + { + return 'cases'; + } + + public function getPrototype(): ClassMemberReflection + { + $unitEnum = $this->declaringClass->getAncestorWithClassName('UnitEnum'); + if ($unitEnum === null) { + throw new ShouldNotHappenException(); + } + + return $unitEnum->getNativeMethod('cases'); + } + + public function getVariants(): array + { + return [ + 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(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + 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; + } + + 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 new file mode 100644 index 0000000000..912b779e67 --- /dev/null +++ b/src/Reflection/Php/EnumPropertyReflection.php @@ -0,0 +1,150 @@ +name; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->declaringClass; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function 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; + } + + public function getWritableType(): Type + { + return $this->type; + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return false; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + 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 a1737af26c..0000000000 --- a/src/Reflection/Php/FakeBuiltinMethodReflection.php +++ /dev/null @@ -1,125 +0,0 @@ -methodName = $methodName; - $this->declaringClass = $declaringClass; - } - - public function getName(): string - { - return $this->methodName; - } - - public function getReflection(): ?\ReflectionMethod - { - return null; - } - - /** - * @return string|false - */ - public function getFileName() - { - return false; - } - - public function getDeclaringClass(): \ReflectionClass - { - return $this->declaringClass; - } - - /** - * @return int|false - */ - public function getStartLine() - { - return false; - } - - /** - * @return int|false - */ - public function getEndLine() - { - return false; - } - - 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; - } - - /** - * @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 299579ff1f..0000000000 --- a/src/Reflection/Php/NativeBuiltinMethodReflection.php +++ /dev/null @@ -1,124 +0,0 @@ -reflection = $reflection; - } - - public function getName(): string - { - return $this->reflection->getName(); - } - - public function getReflection(): ?\ReflectionMethod - { - return $this->reflection; - } - - /** - * @return string|false - */ - public function getFileName() - { - return $this->reflection->getFileName(); - } - - public function getDeclaringClass(): \ReflectionClass - { - return $this->reflection->getDeclaringClass(); - } - - /** - * @return int|false - */ - public function getStartLine() - { - return $this->reflection->getStartLine(); - } - - /** - * @return int|false - */ - public function getEndLine() - { - return $this->reflection->getEndLine(); - } - - 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 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(); - } - - /** - * @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 b75e4eac94..fc0dd70dbe 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -10,78 +10,75 @@ use PHPStan\Analyser\NodeScopeResolver; 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; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\PhpDoc\Tag\ParamTag; 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; use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; +use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\Generic\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 PHPStan\Type\TypeUtils; -use Roave\BetterReflection\Reflection\Adapter\ReflectionMethod; -use Roave\BetterReflection\Reflection\Adapter\ReflectionProperty; - -class PhpClassReflectionExtension +use function array_key_exists; +use function array_keys; +use function array_map; +use function array_slice; +use function count; +use function explode; +use function implode; +use function is_array; +use function sprintf; +use function strtolower; + +final class PhpClassReflectionExtension implements PropertiesClassReflectionExtension, MethodsClassReflectionExtension { - private ScopeFactory $scopeFactory; - - private NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Reflection\Php\PhpMethodReflectionFactory $methodReflectionFactory; - - private \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension; - - private \PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension; - - private \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\PhpDoc\StubPhpDocProvider $stubPhpDocProvider; - - private bool $inferPrivatePropertyTypeFromConstructor; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - /** @var string[] */ - private array $universalObjectCratesClasses; - - /** @var \PHPStan\Reflection\PropertyReflection[][] */ + /** @var ExtendedPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; - /** @var \PHPStan\Reflection\Php\PhpPropertyReflection[][] */ - private array $nativeProperties; + /** @var PhpPropertyReflection[][] */ + private array $nativeProperties = []; - /** @var \PHPStan\Reflection\MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $methodsIncludingAnnotations = []; - /** @var \PHPStan\Reflection\MethodReflection[][] */ + /** @var ExtendedMethodReflection[][] */ private array $nativeMethods = []; /** @var array> */ @@ -90,47 +87,71 @@ class PhpClassReflectionExtension /** @var array */ private array $inferClassConstructorPropertyTypesInProcess = []; - /** - * @param \PHPStan\Analyser\ScopeFactory $scopeFactory - * @param \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver - * @param \PHPStan\Reflection\Php\PhpMethodReflectionFactory $methodReflectionFactory - * @param \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver - * @param \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension - * @param \PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension - * @param \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider - * @param \PHPStan\Parser\Parser $parser - * @param \PHPStan\PhpDoc\StubPhpDocProvider $stubPhpDocProvider - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param bool $inferPrivatePropertyTypeFromConstructor - * @param string[] $universalObjectCratesClasses - */ public function __construct( - ScopeFactory $scopeFactory, - NodeScopeResolver $nodeScopeResolver, - PhpMethodReflectionFactory $methodReflectionFactory, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, - AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, - SignatureMapProvider $signatureMapProvider, - Parser $parser, - StubPhpDocProvider $stubPhpDocProvider, - ReflectionProvider $reflectionProvider, - bool $inferPrivatePropertyTypeFromConstructor, - array $universalObjectCratesClasses + private ScopeFactory $scopeFactory, + private NodeScopeResolver $nodeScopeResolver, + private PhpMethodReflectionFactory $methodReflectionFactory, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private DeprecationProvider $deprecationProvider, + private AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, + private AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, + private SignatureMapProvider $signatureMapProvider, + private Parser $parser, + private StubPhpDocProvider $stubPhpDocProvider, + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private FileTypeMapper $fileTypeMapper, + private AttributeReflectionFactory $attributeReflectionFactory, + private bool $inferPrivatePropertyTypeFromConstructor, ) { - $this->scopeFactory = $scopeFactory; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->methodReflectionFactory = $methodReflectionFactory; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->annotationsMethodsClassReflectionExtension = $annotationsMethodsClassReflectionExtension; - $this->annotationsPropertiesClassReflectionExtension = $annotationsPropertiesClassReflectionExtension; - $this->signatureMapProvider = $signatureMapProvider; - $this->parser = $parser; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->reflectionProvider = $reflectionProvider; - $this->inferPrivatePropertyTypeFromConstructor = $inferPrivatePropertyTypeFromConstructor; - $this->universalObjectCratesClasses = $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 @@ -138,7 +159,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa 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); @@ -150,7 +171,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection { if (!isset($this->nativeProperties[$classReflection->getCacheKey()][$propertyName])) { - /** @var \PHPStan\Reflection\Php\PhpPropertyReflection $property */ + /** @var PhpPropertyReflection $property */ $property = $this->createProperty($classReflection, $propertyName, false); $this->nativeProperties[$classReflection->getCacheKey()][$propertyName] = $property; } @@ -161,30 +182,62 @@ public function getNativeProperty(ClassReflection $classReflection, string $prop private function createProperty( ClassReflection $classReflection, string $propertyName, - bool $includingAnnotations - ): PropertyReflection + bool $includingAnnotations, + ): ExtendedPropertyReflection { $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); $propertyName = $propertyReflection->getName(); $declaringClassName = $propertyReflection->getDeclaringClass()->getName(); $declaringClassReflection = $classReflection->getAncestorWithClassName($declaringClassName); if ($declaringClassReflection === null) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Internal error: Expected to find an ancestor with class name %s on %s, but none was found.', $declaringClassName, - $classReflection->getName() + $classReflection->getName(), )); } - $deprecatedDescription = null; - $isDeprecated = false; + if ($declaringClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($declaringClassReflection->isBackedEnum() && $propertyName === 'value') + ) { + $types = []; + foreach (array_keys($classReflection->getEnumCases()) as $name) { + if ($propertyName === 'name') { + $types[] = new ConstantStringType($name); + continue; + } + + $case = $classReflection->getEnumCase($name); + $value = $case->getBackingValueType(); + if ($value === null) { + throw new ShouldNotHappenException(); + } + + $types[] = $value; + } + + return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, []); + } + } + + $deprecation = $this->deprecationProvider->getPropertyDeprecation($propertyReflection); + $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); + $isDeprecated = $deprecation !== null; $isInternal = false; + $isReadOnlyByPhpDoc = $classReflection->isImmutable(); + $isAllowedPrivateMutation = false; - if ($includingAnnotations && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName)) { + if ( + $includingAnnotations + && !$declaringClassReflection->isEnum() + && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName) + ) { $hierarchyDistances = $classReflection->getClassHierarchyDistances(); $annotationProperty = $this->annotationsPropertiesClassReflectionExtension->getProperty($classReflection, $propertyName); if (!isset($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $distanceDeclaringClass = $propertyReflection->getDeclaringClass()->getName(); @@ -193,10 +246,10 @@ private function createProperty( $distanceDeclaringClass = $propertyTrait; } if (!isset($hierarchyDistances[$distanceDeclaringClass])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { + if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass]) { return $annotationProperty; } } @@ -205,30 +258,35 @@ private function createProperty( ? $propertyReflection->getDocComment() : null; - $declaringTraitName = null; $phpDocType = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findPropertyPhpDoc( - $declaringClassName, - $propertyReflection->getName() - ); - $stubPhpDocString = null; - if ($resolvedPhpDoc === null) { - if ($declaringClassReflection->getFileName() !== false) { - $declaringTraitName = $this->findPropertyTrait($propertyReflection); - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty( - $docComment, - $declaringClassReflection, - $declaringClassReflection->getFileName(), - $declaringTraitName, - $propertyName - ); - $phpDocBlockClassReflection = $declaringClassReflection; + $resolvedPhpDoc = null; + $declaringTraitName = $this->findPropertyTrait($propertyReflection); + $constructorName = null; + if ($propertyReflection->isPromoted()) { + if ($declaringClassReflection->hasConstructor()) { + $constructorName = $declaringClassReflection->getConstructor()->getName(); } - } else { - $phpDocBlockClassReflection = $declaringClassReflection; - $stubPhpDocString = $resolvedPhpDoc->getPhpDocString(); } + 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(); if (isset($varTags[0]) && count($varTags) === 1) { @@ -236,43 +294,131 @@ private function createProperty( } elseif (isset($varTags[$propertyName])) { $phpDocType = $varTags[$propertyName]->getType(); } - if (!isset($phpDocBlockClassReflection)) { - throw new \PHPStan\ShouldNotHappenException(); - } + $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( $phpDocType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createInvariant(), ) : null; - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); + + 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)) { + $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()); + } + $resolvedConstructorPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( + $constructorDocComment, + $declaringClassReflection->getFileName(), + $declaringClassReflection, + $declaringTraitName, + $constructorName, + $positionalParameterNames, + ); + $paramTags = $resolvedConstructorPhpDoc->getParamTags(); + if (isset($paramTags[$propertyReflection->getName()])) { + $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); + } + } } if ( $phpDocType === null && $this->inferPrivatePropertyTypeFromConstructor - && $declaringClassReflection->getFileName() !== false + && $declaringClassReflection->getFileName() !== null && $propertyReflection->isPrivate() - && (!method_exists($propertyReflection, 'hasType') || !$propertyReflection->hasType()) + && !$propertyReflection->isPromoted() + && !$propertyReflection->hasType() && $declaringClassReflection->hasConstructor() && $declaringClassReflection->getConstructor()->getDeclaringClass()->getName() === $declaringClassReflection->getName() ) { $phpDocType = $this->inferPrivatePropertyType( $propertyReflection->getName(), - $declaringClassReflection->getConstructor() + $declaringClassReflection->getConstructor(), ); } $nativeType = null; - if (method_exists($propertyReflection, 'getType') && $propertyReflection->getType() !== null) { + if ($propertyReflection->getType() !== null) { $nativeType = $propertyReflection->getType(); } $declaringTrait = null; + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); if ( - $declaringTraitName !== null && $this->reflectionProvider->hasClass($declaringTraitName) + $declaringTraitName !== null && $reflectionProvider->hasClass($declaringTraitName) ) { - $declaringTrait = $this->reflectionProvider->getClass($declaringTraitName); + $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( @@ -281,10 +427,14 @@ private function createProperty( $nativeType, $phpDocType, $propertyReflection, + $getHook, + $setHook, $deprecatedDescription, $isDeprecated, $isInternal, - $stubPhpDocString + $isReadOnlyByPhpDoc, + $isAllowedPrivateMutation, + $this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())), ); } @@ -293,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; @@ -313,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->reflectionProvider, - $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->reflectionProvider, - $this->universalObjectCratesClasses, - $classReflection - )) { - throw new \PHPStan\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; @@ -366,15 +488,15 @@ public function getNativeMethod(ClassReflection $classReflection, string $method private function createMethod( ClassReflection $classReflection, - BuiltinMethodReflection $methodReflection, - bool $includingAnnotations - ): MethodReflection + ReflectionMethod $methodReflection, + bool $includingAnnotations, + ): ExtendedMethodReflection { if ($includingAnnotations && $this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodReflection->getName())) { $hierarchyDistances = $classReflection->getClassHierarchyDistances(); $annotationMethod = $this->annotationsMethodsClassReflectionExtension->getMethod($classReflection, $methodReflection->getName()); if (!isset($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $distanceDeclaringClass = $methodReflection->getDeclaringClass()->getName(); @@ -383,165 +505,349 @@ private function createMethod( $distanceDeclaringClass = $methodTrait; } if (!isset($hierarchyDistances[$distanceDeclaringClass])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { + if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass]) { return $annotationMethod; } } $declaringClassName = $methodReflection->getDeclaringClass()->getName(); - $signatureMapMethodName = sprintf('%s::%s', $declaringClassName, $methodReflection->getName()); $declaringClass = $classReflection->getAncestorWithClassName($declaringClassName); if ($declaringClass === null) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Internal error: Expected to find an ancestor with class name %s on %s, but none was found.', $declaringClassName, - $classReflection->getName() + $classReflection->getName(), )); } - if ($this->signatureMapProvider->hasFunctionSignature($signatureMapMethodName)) { - $variantName = $signatureMapMethodName; - $variantNames = []; - $i = 0; - while ($this->signatureMapProvider->hasFunctionSignature($variantName)) { - $variantNames[] = $variantName; - $i++; - $variantName = sprintf($signatureMapMethodName . '\'' . $i); + if ( + $declaringClass->isEnum() + && $declaringClass->getName() !== 'UnitEnum' + && strtolower($methodReflection->getName()) === 'cases' + ) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach (array_keys($classReflection->getEnumCases()) as $name) { + $arrayBuilder->setOffsetValueType(null, new EnumCaseObjectType($classReflection->getName(), $name)); } - $stubPhpDocString = null; - $variants = []; - $reflectionMethod = null; - if (class_exists($classReflection->getName(), false)) { - $reflectionClass = new \ReflectionClass($classReflection->getName()); - if ($reflectionClass->hasMethod($methodReflection->getName())) { - $reflectionMethod = $reflectionClass->getMethod($methodReflection->getName()); + return new EnumCasesMethodReflection($declaringClass, $arrayBuilder->getArray()); + } + + if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { + $variantsByType = ['positional' => []]; + $throwType = null; + $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; } - } else { - $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodReflection->getName()); - } - foreach ($variantNames as $innerVariantName) { - $methodSignature = $this->signatureMapProvider->getFunctionSignature($innerVariantName, $declaringClassName); - $phpDocReturnType = null; - $stubPhpDocParameterTypes = []; - $stubPhpDocParameterVariadicity = []; - if (count($variantNames) === 1) { - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static function (ParameterSignature $parameterSignature): string { - return $parameterSignature->getName(); - }, $methodSignature->getParameters())); - if ($stubPhpDocPair !== null) { - [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; - $stubPhpDocString = $stubPhpDoc->getPhpDocString(); - $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); - $returnTag = $stubPhpDoc->getReturnTag(); - if ($returnTag !== null) { - $stubPhpDocReturnType = $returnTag->getType(); - $phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( - $stubPhpDocReturnType, - $templateTypeMap - ); - } - foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { - $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramTag->getType(), - $templateTypeMap + 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(), + ); + } + + if ($declaringClassName === $stubDeclaringClass->getName() && $stubPhpDoc->hasPhpDocString()) { + $phpDocComment = $stubPhpDoc->getPhpDocString(); + } + } + } + if ($stubPhpDocPair === null && $methodReflection->getDocComment() !== false) { + $filename = $methodReflection->getFileName(); + if ($filename !== false) { + $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( + $filename, + $declaringClassName, + null, + $methodReflection->getName(), + $methodReflection->getDocComment(), ); - $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); + $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(); + } + + if ($phpDocBlock->hasPhpDocString()) { + $phpDocComment = $phpDocBlock->getPhpDocString(); + } + + 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, $phpDocReturnType, $reflectionMethod); } - if ($this->signatureMapProvider->hasFunctionMetadata($signatureMapMethodName)) { - $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getFunctionMetadata($signatureMapMethodName)['hasSideEffects']); + if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { + $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']); } else { $hasSideEffects = TrinaryLogic::createMaybe(); } return new NativeMethodReflection( - $this->reflectionProvider, + $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, $methodReflection, - $variants, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $hasSideEffects, - $stubPhpDocString + $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 function (\ReflectionParameter $parameter): string { - return $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; } - $stubPhpDocString = null; if ($resolvedPhpDoc === null) { - if ($declaringClass->getFileName() !== false) { - $docComment = $methodReflection->getDocComment(); - $positionalParameterNames = array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $methodReflection->getParameters()); - - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( - $docComment, - $declaringClass->getFileName(), - $declaringClass, - $declaringTraitName, - $methodReflection->getName(), - $positionalParameterNames - ); - $phpDocBlockClassReflection = $declaringClass; - } - } else { - $stubPhpDocString = $resolvedPhpDoc->getPhpDocString(); + $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; + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); if ( - $declaringTraitName !== null && $this->reflectionProvider->hasClass($declaringTraitName) + $declaringTraitName !== null && $reflectionProvider->hasClass($declaringTraitName) ) { - $declaringTrait = $this->reflectionProvider->getClass($declaringTraitName); + $declaringTrait = $reflectionProvider->getClass($declaringTraitName); } - $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; - $phpDocReturnType = null; - $phpDocThrowType = null; - $deprecatedDescription = null; - $isDeprecated = false; - $isInternal = false; - $isFinal = false; - if ($resolvedPhpDoc !== null) { - $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTypes = array_map(static function (ParamTag $tag) use ($phpDocBlockClassReflection): Type { - return TemplateTypeHelper::resolveTemplateTypes( - $tag->getType(), - $phpDocBlockClassReflection->getActiveTemplateTypeMap() + if ($methodReflection->isConstructor()) { + foreach ($methodReflection->getParameters() as $parameter) { + if (!$parameter->isPromoted()) { + continue; + } + + if (!$methodReflection->getDeclaringClass()->hasProperty($parameter->getName())) { + continue; + } + + $parameterProperty = $methodReflection->getDeclaringClass()->getProperty($parameter->getName()); + if (!$parameterProperty->isPromoted()) { + continue; + } + if ($parameterProperty->getDocComment() === false) { + continue; + } + + $propertyDocblock = $this->fileTypeMapper->getResolvedPhpDoc( + $fileDeclaringClass->getFileName(), + $fileDeclaringClass->getName(), + $declaringTraitName, + $methodReflection->getName(), + $parameterProperty->getDocComment(), ); - }, $resolvedPhpDoc->getParamTags()); - $nativeReturnType = TypehintHelper::decideTypeFromReflection( - $methodReflection->getReturnType(), - null, - $declaringClass->getName() + $varTags = $propertyDocblock->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $phpDocType = $varTags[0]->getType(); + } elseif (isset($varTags[$parameter->getName()])) { + $phpDocType = $varTags[$parameter->getName()]->getType(); + } else { + continue; + } + + $phpDocParameterTypes[$parameter->getName()] = $phpDocType; + } + } + + $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; + } + $phpDocParameterTypes[$paramName] = $paramTag->getType(); + } + foreach ($phpDocParameterTypes as $paramName => $paramType) { + $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ); + } + + $phpDocParameterOutTypes = []; + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ); - $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); - $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; + } + + $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(); + } + $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, @@ -552,210 +858,157 @@ private function createMethod( $isDeprecated, $isInternal, $isFinal, - $stubPhpDocString + $isPure, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $immediatelyInvokedCallableParameters, + $closureThisParameters, + $acceptsNamedArguments, + $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $actualDeclaringClass->getFileName())), ); } /** - * @param FunctionSignature $methodSignature * @param array $stubPhpDocParameterTypes * @param array $stubPhpDocParameterVariadicity - * @param Type|null $phpDocReturnType - * @param \ReflectionMethod|null $reflectionMethod - * @return FunctionVariantWithPhpDocs + * @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, array $stubPhpDocParameterTypes, array $stubPhpDocParameterVariadicity, + ?Type $stubPhpDocReturnType, + array $phpDocParameterTypes, ?Type $phpDocReturnType, - ?\ReflectionMethod $reflectionMethod - ): FunctionVariantWithPhpDocs + array $phpDocParameterNameMapping, + array $stubPhpDocParameterOutTypes, + array $phpDocParameterOutTypes, + array $stubImmediatelyInvokedCallableParameters, + array $immediatelyInvokedCallableParameters, + array $stubClosureThisParameters, + array $closureThisParameters, + bool $usePhpDocParameterNames, + ): ExtendedFunctionVariant { $parameters = []; - $nativeParameters = null; - $nativeReturnType = null; - if ($reflectionMethod !== null) { - $nativeParameters = $reflectionMethod->getParameters(); - $nativeReturnType = TypehintHelper::decideTypeFromReflection( - $reflectionMethod->getReturnType(), - null, - null - ); - } - foreach ($methodSignature->getParameters() as $i => $parameterSignature) { - if ( - $nativeParameters !== null - && array_key_exists($i, $nativeParameters) - ) { - $nativeParameterType = TypehintHelper::decideTypeFromReflection( - $nativeParameters[$i]->getType(), - null, - null, - $nativeParameters[$i]->isVariadic() - ); + foreach ($methodSignature->getParameters() as $parameterSignature) { + $type = null; + $phpDocType = null; + $parameterOutType = null; + + $phpDocParameterName = $phpDocParameterNameMapping[$parameterSignature->getName()] ?? $parameterSignature->getName(); + + if (isset($stubPhpDocParameterTypes[$parameterSignature->getName()])) { + $type = $stubPhpDocParameterTypes[$parameterSignature->getName()]; + $phpDocType = $stubPhpDocParameterTypes[$parameterSignature->getName()]; + } elseif (isset($phpDocParameterTypes[$phpDocParameterName])) { + $phpDocType = $phpDocParameterTypes[$phpDocParameterName]; + } + + if (isset($stubPhpDocParameterOutTypes[$parameterSignature->getName()])) { + $parameterOutType = $stubPhpDocParameterOutTypes[$parameterSignature->getName()]; + } elseif (isset($phpDocParameterOutTypes[$phpDocParameterName])) { + $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; + } + + if (isset($stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()])) { + $immediatelyInvoked = $stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()]; + } elseif (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { + $immediatelyInvoked = $immediatelyInvokedCallableParameters[$phpDocParameterName]; } else { - $nativeParameterType = new MixedType(); + $immediatelyInvoked = TrinaryLogic::createMaybe(); } - $parameters[] = new NativeParameterWithPhpDocsReflection( - $parameterSignature->getName(), + + $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(), - $stubPhpDocParameterTypes[$parameterSignature->getName()] ?? $parameterSignature->getType(), - $stubPhpDocParameterTypes[$parameterSignature->getName()] ?? new MixedType(), - $nativeParameterType, + $type ?? $parameterSignature->getType(), + $phpDocType ?? new MixedType(), + $parameterSignature->getNativeType(), $parameterSignature->passedByReference(), $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), - null + $parameterSignature->getDefaultValue(), + $parameterOutType ?? $parameterSignature->getOutType(), + $immediatelyInvoked, + $closureThisType, + [], ); } - return new FunctionVariantWithPhpDocs( + if ($stubPhpDocReturnType !== null) { + $returnType = $stubPhpDocReturnType; + $phpDocReturnType = $stubPhpDocReturnType; + } else { + $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); + } + + return new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, $parameters, $methodSignature->isVariadic(), - $phpDocReturnType ?? $methodSignature->getReturnType(), + $returnType, $phpDocReturnType ?? new MixedType(), - $nativeReturnType ?? 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 - * @param \ReflectionProperty $propertyReflection - * @return string|null - */ - 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(); + $declaringClass = $methodReflection->getBetterReflection()->getDeclaringClass(); + if ($declaringClass->isTrait()) { + if ($methodReflection->getDeclaringClass()->isTrait() && $declaringClass->getName() === $methodReflection->getDeclaringClass()->getName()) { + return null; } - 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; - } - - if ( - $methodReflection->getFileName() === $traitReflection->getFileName() - && $methodReflection->getStartLine() >= $traitReflection->getStartLine() - && $methodReflection->getEndLine() <= $traitReflection->getEndLine() - ) { - return $traitReflection->getName(); - } + return $declaringClass->getName(); } return null; } - /** - * @param \ReflectionClass $class - * @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 - ): Type + MethodReflection $constructor, + ): ?Type { $declaringClassName = $constructor->getDeclaringClass()->getName(); if (isset($this->inferClassConstructorPropertyTypesInProcess[$declaringClassName])) { - return new MixedType(); + return null; } $this->inferClassConstructorPropertyTypesInProcess[$declaringClassName] = true; $propertyTypes = $this->inferAndCachePropertyTypes($constructor); @@ -764,22 +1017,21 @@ private function inferPrivatePropertyType( return $propertyTypes[$propertyName]; } - return new MixedType(); + return null; } /** - * @param \PHPStan\Reflection\MethodReflection $constructor * @return array */ private function inferAndCachePropertyTypes( - MethodReflection $constructor + MethodReflection $constructor, ): array { $declaringClass = $constructor->getDeclaringClass(); if (isset($this->propertyTypesCache[$declaringClass->getName()])) { return $this->propertyTypesCache[$declaringClass->getName()]; } - if ($declaringClass->getFileName() === false) { + if ($declaringClass->getFileName() === null) { return $this->propertyTypesCache[$declaringClass->getName()] = []; } @@ -791,24 +1043,22 @@ private function inferAndCachePropertyTypes( } $methodNode = $this->findConstructorNode($constructor->getName(), $classNode->stmts); - if ($methodNode === null || $methodNode->stmts === null) { + if ($methodNode === null || $methodNode->stmts === null || count($methodNode->stmts) === 0) { return $this->propertyTypesCache[$declaringClass->getName()] = []; } $classNameParts = explode('\\', $declaringClass->getName()); $namespace = null; - if (count($classNameParts) > 0) { + if (count($classNameParts) > 1) { $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] = $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, @@ -818,7 +1068,15 @@ private function inferAndCachePropertyTypes( $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal + $isFinal, + $isPure, + $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $propertyTypes = []; @@ -850,8 +1108,8 @@ private function inferAndCachePropertyTypes( continue; } - $propertyType = TypeUtils::generalizeType($propertyType); - if ($propertyType instanceof ConstantArrayType) { + $propertyType = $propertyType->generalize(GeneralizePrecision::lessSpecific()); + if ($propertyType->isConstantArray()->yes()) { $propertyType = new ArrayType(new MixedType(true), new MixedType(true)); } @@ -862,15 +1120,14 @@ private function inferAndCachePropertyTypes( } /** - * @param string $className - * @param \PhpParser\Node[] $nodes - * @return \PhpParser\Node\Stmt\Class_|null + * @param Node[] $nodes */ private function findClassNode(string $className, array $nodes): ?Class_ { foreach ($nodes as $node) { if ( $node instanceof Class_ + && $node->namespacedName !== null && $node->namespacedName->toString() === $className ) { return $node; @@ -898,9 +1155,7 @@ private function findClassNode(string $className, array $nodes): ?Class_ } /** - * @param string $methodName - * @param \PhpParser\Node\Stmt[] $classStatements - * @return \PhpParser\Node\Stmt\ClassMethod|null + * @param Node\Stmt[] $classStatements */ private function findConstructorNode(string $methodName, array $classStatements): ?ClassMethod { @@ -926,7 +1181,9 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection $phpDocReturnType = $returnTag->getType(); $phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( $phpDocReturnType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ); if ($returnTag->isExplicit() || $nativeReturnType->isSuperTypeOf($phpDocReturnType)->yes()) { @@ -937,15 +1194,18 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection } /** - * @param ClassReflection $declaringClass - * @param string $methodName * @param array $positionalParameterNames - * @return array{\PHPStan\PhpDoc\ResolvedPhpDocBlock, ClassReflection}|null + * @return array{ResolvedPhpDocBlock, ClassReflection}|null */ - private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringClass, string $methodName, array $positionalParameterNames): ?array + 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]; } @@ -962,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 a9194e6fe4..02a5b642af 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -2,98 +2,77 @@ namespace PHPStan\Reflection\Php; +use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt\ClassMethod; -use PHPStan\Reflection\FunctionVariantWithPhpDocs; +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\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\TypeCombinator; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; - -class PhpFunctionFromParserNodeReflection implements \PHPStan\Reflection\FunctionReflection +use function array_reverse; +use function is_array; +use function is_string; + +/** + * @api + */ +class PhpFunctionFromParserNodeReflection implements FunctionReflection, ExtendedParametersAcceptor { - private \PhpParser\Node\FunctionLike $functionLike; - - private \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap; - - /** @var \PHPStan\Type\Type[] */ - private array $realParameterTypes; - - /** @var \PHPStan\Type\Type[] */ - private array $phpDocParameterTypes; - - /** @var \PHPStan\Type\Type[] */ - private array $realParameterDefaultValues; - - private bool $realReturnTypePresent; - - private \PHPStan\Type\Type $realReturnType; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $throwType; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private bool $isFinal; + /** @var Function_|ClassMethod|Node\PropertyHook */ + private Node\FunctionLike $functionLike; - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; /** - * @param FunctionLike $functionLike - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $realParameterTypes - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param \PHPStan\Type\Type[] $realParameterDefaultValues - * @param bool $realReturnTypePresent - * @param Type $realReturnType - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal + * @param 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, - TemplateTypeMap $templateTypeMap, - array $realParameterTypes, - array $phpDocParameterTypes, - array $realParameterDefaultValues, - bool $realReturnTypePresent, - Type $realReturnType, - ?Type $phpDocReturnType = null, - ?Type $throwType = null, - ?string $deprecatedDescription = null, - bool $isDeprecated = false, - bool $isInternal = false, - bool $isFinal = false + private string $fileName, + private TemplateTypeMap $templateTypeMap, + 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, + 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; - $this->templateTypeMap = $templateTypeMap; - $this->realParameterTypes = $realParameterTypes; - $this->phpDocParameterTypes = $phpDocParameterTypes; - $this->realParameterDefaultValues = $realParameterDefaultValues; - $this->realReturnTypePresent = $realReturnTypePresent; - $this->realReturnType = $realReturnType; - $this->phpDocReturnType = $phpDocReturnType; - $this->throwType = $throwType; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; } protected function getFunctionLike(): FunctionLike @@ -101,30 +80,41 @@ protected function getFunctionLike(): FunctionLike return $this->functionLike; } + public function getFileName(): string + { + return $this->fileName; + } + public function getName(): string { if ($this->functionLike instanceof ClassMethod) { return $this->functionLike->name->name; } + if (!$this->functionLike instanceof Function_) { + // PropertyHook is handled in PhpMethodFromParserNodeReflection subclass + throw new ShouldNotHappenException(); + } + + if ($this->functionLike->namespacedName === null) { + throw new ShouldNotHappenException(); + } + return (string) $this->functionLike->namespacedName; } - /** - * @return \PHPStan\Reflection\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(), ), ]; } @@ -132,23 +122,56 @@ 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 \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] + * @return list */ - private function getParameters(): array + public function getParameters(): array { $parameters = []; $isOptional = true; - /** @var \PhpParser\Node\Param $parameter */ + /** @var Node\Param $parameter */ foreach (array_reverse($this->functionLike->getParams()) as $parameter) { if ($parameter->default === null && !$parameter->variadic) { $isOptional = false; } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + 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, @@ -158,14 +181,18 @@ private function getParameters(): array ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $this->realParameterDefaultValues[$parameter->var->name] ?? null, - $parameter->variadic + $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) { @@ -176,17 +203,24 @@ private function isVariadic(): bool return false; } - protected function getReturnType(): Type + public function getReturnType(): Type { - $phpDocReturnType = $this->phpDocReturnType; - if ( - $this->realReturnTypePresent - && $phpDocReturnType !== null - && TypeCombinator::containsNull($this->realReturnType) !== TypeCombinator::containsNull($phpDocReturnType) - ) { - $phpDocReturnType = null; - } - return TypehintHelper::decideType($this->realReturnType, $phpDocReturnType); + 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 @@ -208,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; @@ -224,10 +249,89 @@ 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) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + return TrinaryLogic::createMaybe(); } + public function isBuiltin(): bool + { + return false; + } + + 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_) { + return true; + } + + if ($node instanceof Node\Expr\YieldFrom) { + return true; + } + + foreach ($node->getSubNodeNames() as $nodeName) { + $nodeProperty = $node->$nodeName; + + if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) { + return true; + } + + if (!is_array($nodeProperty)) { + continue; + } + + foreach ($nodeProperty as $nodePropertyArrayItem) { + if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) { + return true; + } + } + } + + return false; + } + + 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 22777c8c08..368ac3f471 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -2,104 +2,69 @@ namespace PHPStan\Reflection\Php; -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\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\ReflectionWithFilename; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; +use function array_key_exists; +use function array_map; +use function count; +use function is_array; +use function is_file; -class PhpFunctionReflection implements FunctionReflection, ReflectionWithFilename +final class PhpFunctionReflection implements FunctionReflection { - private \ReflectionFunction $reflection; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - private \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap; - - /** @var \PHPStan\Type\Type[] */ - private array $phpDocParameterTypes; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $phpDocThrowType; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private bool $isFinal; - - /** @var string|false */ - private $filename; - - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; + private ?bool $containsVariadicCalls = null; + /** - * @param \ReflectionFunction $reflection - * @param Parser $parser - * @param FunctionCallStatementFinder $functionCallStatementFinder - * @param Cache $cache - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|false $filename + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes + * @param list $attributes */ public function __construct( - \ReflectionFunction $reflection, - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - $filename + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionFunction $reflection, + private Parser $parser, + private AttributeReflectionFactory $attributeReflectionFactory, + private TemplateTypeMap $templateTypeMap, + private array $phpDocParameterTypes, + private ?Type $phpDocReturnType, + private ?Type $phpDocThrowType, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, + 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, ) { - $this->reflection = $reflection; - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - $this->templateTypeMap = $templateTypeMap; - $this->phpDocParameterTypes = $phpDocParameterTypes; - $this->phpDocReturnType = $phpDocReturnType; - $this->phpDocThrowType = $phpDocThrowType; - $this->isDeprecated = $isDeprecated; - $this->deprecatedDescription = $deprecatedDescription; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; - $this->filename = $filename; } public function getName(): string @@ -107,29 +72,31 @@ public function getName(): string return $this->reflection->getName(); } - /** - * @return string|false - */ - public function getFileName() + public function getFileName(): ?string { + if ($this->filename === null) { + return null; + } + + if (!is_file($this->filename)) { + return null; + } + return $this->filename; } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { if ($this->variants === null) { $this->variants = [ - new FunctionVariantWithPhpDocs( + new ExtendedFunctionVariant( $this->templateTypeMap, null, $this->getParameters(), $this->isVariadic(), $this->getReturnType(), $this->getPhpDocReturnType(), - $this->getNativeReturnType() + $this->getNativeReturnType(), ), ]; } @@ -137,16 +104,36 @@ public function getVariants(): array return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** - * @return \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] + * @return list */ private function getParameters(): array { - return array_map(function (\ReflectionParameter $reflection): PhpParameterReflection { + 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 + null, + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, + $immediatelyInvokedCallable, + $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, + $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), ); }, $this->reflection->getParameters()); } @@ -154,77 +141,40 @@ private function getParameters(): array private function isVariadic(): bool { $isNativelyVariadic = $this->reflection->isVariadic(); - if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) { - $fileName = $this->reflection->getFileName(); - $functionName = $this->reflection->getName(); - $modifiedTime = filemtime($fileName); - if ($modifiedTime === false) { - $modifiedTime = time(); - } - $variableCacheKey = sprintf('%d-v1', $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; - } - - /** - * @param \PhpParser\Node[] $nodes - * @return bool - */ - private function callsFuncGetArgs(array $nodes): bool - { - foreach ($nodes as $node) { - if ($node instanceof Function_) { - $functionName = (string) $node->namespacedName; - - if ($functionName === $this->reflection->getName()) { - return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null; - } + 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 { - if ($this->reflection->getName() === 'count') { - return new IntegerType(); - } - return TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), - $this->phpDocReturnType + $this->phpDocReturnType, ); } @@ -248,13 +198,18 @@ 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 TrinaryLogic::createFromBoolean( - $this->isDeprecated || $this->reflection->isDeprecated() + $this->isDeprecated || $this->reflection->isDeprecated(), ); } @@ -263,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; @@ -275,10 +225,53 @@ 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) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + 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 39defe0f43..cd0e6fcbf6 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -2,102 +2,141 @@ namespace PHPStan\Reflection\Php; +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 { - private \PHPStan\Reflection\ClassReflection $declaringClass; - /** - * @param ClassReflection $declaringClass - * @param ClassMethod $classMethod - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $realParameterTypes - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param \PHPStan\Type\Type[] $realParameterDefaultValues - * @param bool $realReturnTypePresent - * @param Type $realReturnType - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal + * @param Type[] $realParameterTypes + * @param Type[] $phpDocParameterTypes + * @param Type[] $realParameterDefaultValues + * @param array> $parameterAttributes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( - ClassReflection $declaringClass, - ClassMethod $classMethod, + private ClassReflection $declaringClass, + private ClassMethod|Node\PropertyHook $classMethod, + private ?string $hookForProperty, + string $fileName, TemplateTypeMap $templateTypeMap, array $realParameterTypes, array $phpDocParameterTypes, array $realParameterDefaultValues, - bool $realReturnTypePresent, + 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' - ) { - $realReturnTypePresent = true; + if ($this->isConstructor) { + $realReturnType = new VoidType(); + } + if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) { $realReturnType = new VoidType(); } if ($name === '__tostring') { - $realReturnTypePresent = true; $realReturnType = new StringType(); } if ($name === '__isset') { - $realReturnTypePresent = true; $realReturnType = new BooleanType(); } if ($name === '__sleep') { - $realReturnTypePresent = true; $realReturnType = new ArrayType(new IntegerType(), new StringType()); } if ($name === '__set_state') { - $realReturnTypePresent = true; $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, + $fileName, $templateTypeMap, $realParameterTypes, $phpDocParameterTypes, $realParameterDefaultValues, - $realReturnTypePresent, + $parameterAttributes, $realReturnType, $phpDocReturnType, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, - $isFinal || $classMethod->isFinal() + $isPure, + $acceptsNamedArguments, + $assertions, + $phpDocComment, + $parameterOutTypes, + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, + $attributes, ); - $this->declaringClass = $declaringClass; } public function getDeclaringClass(): ClassReflection @@ -109,36 +148,152 @@ public function getPrototype(): ClassMemberReflection { try { return $this->declaringClass->getNativeMethod($this->getClassMethod()->name->name)->getPrototype(); - } catch (\PHPStan\Reflection\MissingMethodFromReflectionException $e) { + } catch (MissingMethodFromReflectionException) { return $this; } } - private function getClassMethod(): ClassMethod + private function getClassMethod(): ClassMethod|Node\PropertyHook { - /** @var \PhpParser\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 getDocComment(): ?string + public function isFinal(): TrinaryLogic { - return null; + $method = $this->getClassMethod(); + if ($method instanceof Node\PropertyHook) { + return TrinaryLogic::createFromBoolean($method->isFinal()); + } + + return TrinaryLogic::createFromBoolean($method->isFinal() || $this->isFinal); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isFinal()); + } + + 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 833eeef81c..432fd69350 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection\Php; -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\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; @@ -25,108 +28,73 @@ 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; - -class PhpMethodReflection implements MethodReflection +use ReflectionException; +use function array_key_exists; +use function array_map; +use function count; +use function explode; +use function in_array; +use function is_array; +use function sprintf; +use function strtolower; +use const PHP_VERSION_ID; + +/** + * @api + */ +final class PhpMethodReflection implements ExtendedMethodReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private ?ClassReflection $declaringTrait; - - private BuiltinMethodReflection $reflection; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - private \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap; - - /** @var \PHPStan\Type\Type[] */ - private array $phpDocParameterTypes; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $phpDocThrowType; - - /** @var \PHPStan\Reflection\Php\PhpParameterReflection[]|null */ + /** @var list|null */ private ?array $parameters = null; - private ?\PHPStan\Type\Type $returnType = null; - - private ?\PHPStan\Type\Type $nativeReturnType = null; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; + private ?Type $returnType = null; - private bool $isInternal; + private ?Type $nativeReturnType = null; - private bool $isFinal; - - private ?string $stubPhpDocString; - - /** @var FunctionVariantWithPhpDocs[]|null */ + /** @var list|null */ private ?array $variants = null; + private ?bool $containsVariadicCalls = null; + /** - * @param ClassReflection $declaringClass - * @param ClassReflection|null $declaringTrait - * @param BuiltinMethodReflection $reflection - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param Parser $parser - * @param FunctionCallStatementFinder $functionCallStatementFinder - * @param Cache $cache - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|null $stubPhpDocString + * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters + * @param list $attributes */ public function __construct( - ClassReflection $declaringClass, - ?ClassReflection $declaringTrait, - BuiltinMethodReflection $reflection, - ReflectionProvider $reflectionProvider, - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - ?string $stubPhpDocString + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ClassReflection $declaringClass, + private ?ClassReflection $declaringTrait, + private ReflectionMethod $reflection, + private ReflectionProvider $reflectionProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + private Parser $parser, + private TemplateTypeMap $templateTypeMap, + private array $phpDocParameterTypes, + private ?Type $phpDocReturnType, + private ?Type $phpDocThrowType, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, + private bool $isFinal, + private ?bool $isPure, + private Assertions $asserts, + private bool $acceptsNamedArguments, + private ?Type $selfOutType, + private ?string $phpDocComment, + private array $phpDocParameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, + private array $attributes, ) { - $this->declaringClass = $declaringClass; - $this->declaringTrait = $declaringTrait; - $this->reflection = $reflection; - $this->reflectionProvider = $reflectionProvider; - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - $this->templateTypeMap = $templateTypeMap; - $this->phpDocParameterTypes = $phpDocParameterTypes; - $this->phpDocReturnType = $phpDocReturnType; - $this->phpDocThrowType = $phpDocThrowType; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; - $this->stubPhpDocString = $stubPhpDocString; } public function getDeclaringClass(): ClassReflection @@ -139,23 +107,26 @@ public function getDeclaringTrait(): ?ClassReflection return $this->declaringTrait; } - public function getDocComment(): ?string - { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; - } - - return $this->reflection->getDocComment(); - } - /** - * @return self|\PHPStan\Reflection\MethodPrototypeReflection + * @return self|MethodPrototypeReflection */ public function getPrototype(): ClassMemberReflection { 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(), null, $prototypeDeclaringClass); + } return new MethodPrototypeReflection( $prototypeMethod->getName(), @@ -165,9 +136,11 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), - $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants() + $prototypeMethod->isInternal(), + $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), + $tentativeReturnType, ); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return $this; } } @@ -182,6 +155,10 @@ public function getName(): string $name = $this->reflection->getName(); $lowercaseName = strtolower($name); if ($lowercaseName === $name) { + if (PHP_VERSION_ID >= 80000) { + return $name; + } + // fix for https://bugs.php.net/bug.php?id=74939 foreach ($this->getDeclaringClass()->getNativeReflection()->getTraitAliases() as $traitTarget) { $correctName = $this->getMethodNameWithCorrectCase($name, $traitTarget); @@ -214,20 +191,20 @@ 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(), $this->isVariadic(), $this->getReturnType(), $this->getPhpDocReturnType(), - $this->getNativeReturnType() + $this->getNativeReturnType(), ), ]; } @@ -235,19 +212,32 @@ public function getVariants(): array return $this->variants; } + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** - * @return \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] + * @return list */ private function getParameters(): array { if ($this->parameters === null) { - $this->parameters = array_map(function (\ReflectionParameter $reflection): PhpParameterReflection { - return new PhpParameterReflection( - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - $this->getDeclaringClass()->getName() - ); - }, $this->reflection->getParameters()); + $this->parameters = array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( + $this->initializerExprTypeResolver, + $reflection, + $this->phpDocParameterTypes[$reflection->getName()] ?? null, + $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()); } return $this->parameters; @@ -263,84 +253,40 @@ private function isVariadic(): bool $filename = $this->declaringTrait->getFileName(); } - if (!$isNativelyVariadic && $filename !== false) { - $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-v2', $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 ClassReflection $declaringClass - * @param \PhpParser\Node[] $nodes - * @return bool - */ - private function callsFuncGetArgs(ClassReflection $declaringClass, array $nodes): bool - { - foreach ($nodes as $node) { - if ( - $node instanceof \PhpParser\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 + 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()]; } - $methodName = $node->name->name; - if ($methodName === $this->reflection->getName()) { - return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null; - } - - 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 @@ -357,32 +303,29 @@ private function getReturnType(): Type { if ($this->returnType === null) { $name = strtolower($this->getName()); - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { - return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); - } - if ($name === '__tostring') { - return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType); - } - if ($name === '__isset') { - return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType); - } - if ($name === '__sleep') { - return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType); - } - if ($name === '__set_state') { - return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType); + $returnType = $this->reflection->getReturnType(); + if ($returnType === null) { + if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { + return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); + } + if ($name === '__tostring') { + return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType); + } + if ($name === '__isset') { + return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType); + } + if ($name === '__sleep') { + return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType); + } + if ($name === '__set_state') { + return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType); + } } $this->returnType = TypehintHelper::decideTypeFromReflection( - $this->reflection->getReturnType(), + $returnType, $this->phpDocReturnType, - $this->declaringClass->getName() + $this->declaringClass, ); } @@ -404,7 +347,7 @@ private function getNativeReturnType(): Type $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), null, - $this->declaringClass->getName() + $this->declaringClass, ); } @@ -417,22 +360,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 @@ -447,10 +409,125 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ( + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() + ) { return TrinaryLogic::createYes(); } + if ($this->isPure !== null) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function 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 801409c55f..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; @@ -10,25 +14,16 @@ interface PhpMethodReflectionFactory { /** - * @param \PHPStan\Reflection\ClassReflection $declaringClass - * @param \PHPStan\Reflection\ClassReflection|null $declaringTrait - * @param BuiltinMethodReflection $reflection - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param \PHPStan\Type\Type|null $phpDocReturnType - * @param \PHPStan\Type\Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|null $stubPhpDocString - * - * @return \PHPStan\Reflection\Php\PhpMethodReflection + * @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, @@ -37,7 +32,15 @@ public function create( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?string $stubPhpDocString + ?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 6970ac1c70..f048ea7100 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,48 +2,37 @@ namespace PHPStan\Reflection\Php; +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 \PHPStan\Reflection\ParameterReflectionWithPhpDocs +final class PhpParameterFromParserNodeReflection implements ExtendedParameterReflection { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $realType; - - private ?\PHPStan\Type\Type $phpDocType; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private ?\PHPStan\Type\Type $defaultValue; - - private bool $variadic; - - private ?\PHPStan\Type\Type $type = null; + private ?Type $type = null; + /** + * @param list $attributes + */ public function __construct( - string $name, - bool $optional, - Type $realType, - ?Type $phpDocType, - PassedByReference $passedByReference, - ?Type $defaultValue, - bool $variadic + private string $name, + private bool $optional, + private Type $realType, + private ?Type $phpDocType, + private PassedByReference $passedByReference, + private ?Type $defaultValue, + private bool $variadic, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, + private array $attributes, ) { - $this->name = $name; - $this->optional = $optional; - $this->realType = $realType; - $this->phpDocType = $phpDocType; - $this->passedByReference = $passedByReference; - $this->defaultValue = $defaultValue; - $this->variadic = $variadic; } public function getName(): string @@ -61,8 +50,11 @@ public function getType(): Type if ($this->type === null) { $phpDocType = $this->phpDocType; if ($phpDocType !== null && $this->defaultValue !== null) { - if ($this->defaultValue instanceof NullType) { - $phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType); + if ($this->defaultValue->isNull()->yes()) { + $inferred = $phpDocType->inferTemplateTypes($this->defaultValue); + if ($inferred->isEmpty()) { + $phpDocType = TypeCombinator::addNull($phpDocType); + } } } $this->type = TypehintHelper::decideType($this->realType, $phpDocType); @@ -76,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; @@ -96,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 c134d7a1b2..01f69e3ccb 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -2,35 +2,40 @@ 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; -class PhpParameterReflection implements ParameterReflectionWithPhpDocs +final class PhpParameterReflection implements ExtendedParameterReflection { - private \ReflectionParameter $reflection; + private ?Type $type = null; - private ?\PHPStan\Type\Type $phpDocType; - - private ?\PHPStan\Type\Type $type = null; - - private ?\PHPStan\Type\Type $nativeType = null; - - private ?string $declaringClassName; + private ?Type $nativeType = null; + /** + * @param list $attributes + */ public function __construct( - \ReflectionParameter $reflection, - ?Type $phpDocType, - ?string $declaringClassName + private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionParameter $reflection, + private ?Type $phpDocType, + private ?ClassReflection $declaringClass, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, + private array $attributes, ) { - $this->reflection = $reflection; - $this->phpDocType = $phpDocType; - $this->declaringClassName = $declaringClassName; } public function isOptional(): bool @@ -47,21 +52,24 @@ public function getType(): Type { if ($this->type === null) { $phpDocType = $this->phpDocType; - if ($phpDocType !== null) { - try { - if ($this->reflection->isDefaultValueAvailable() && $this->reflection->getDefaultValue() === null) { - $phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType); - } - } catch (\Throwable $e) { - // 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->isVariadic() + $this->declaringClass, + $this->isVariadic(), ); } @@ -89,14 +97,19 @@ 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->isVariadic() + $this->declaringClass, + $this->isVariadic(), ); } @@ -105,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 $e) { - 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 e8a29a18a8..8307f36d7b 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -2,59 +2,55 @@ 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 function sprintf; -class PhpPropertyReflection implements PropertyReflection +/** + * @api + */ +final class PhpPropertyReflection implements ExtendedPropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; + private ?Type $finalNativeType = null; - private ?\PHPStan\Reflection\ClassReflection $declaringTrait; - - private ?\ReflectionType $nativeType; - - private ?\PHPStan\Type\Type $finalNativeType = null; - - private ?\PHPStan\Type\Type $phpDocType; - - private ?\PHPStan\Type\Type $type = null; - - private \ReflectionProperty $reflection; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private ?string $stubPhpDocString; + private ?Type $type = null; + /** + * @param list $attributes + */ public function __construct( - ClassReflection $declaringClass, - ?ClassReflection $declaringTrait, - ?\ReflectionType $nativeType, - ?Type $phpDocType, - \ReflectionProperty $reflection, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - ?string $stubPhpDocString + private ClassReflection $declaringClass, + private ?ClassReflection $declaringTrait, + private 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, ) { - $this->declaringClass = $declaringClass; - $this->declaringTrait = $declaringTrait; - $this->nativeType = $nativeType; - $this->phpDocType = $phpDocType; - $this->reflection = $reflection; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->stubPhpDocString = $stubPhpDocString; + } + + public function getName(): string + { + return $this->reflection->getName(); } public function getDeclaringClass(): ClassReflection @@ -69,10 +65,6 @@ public function getDeclaringTrait(): ?ClassReflection public function getDocComment(): ?string { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; - } - $docComment = $this->reflection->getDocComment(); if ($docComment === false) { return null; @@ -96,13 +88,23 @@ public function isPublic(): bool return $this->reflection->isPublic(); } + public function isReadOnly(): bool + { + return $this->reflection->isReadOnly(); + } + + public function isReadOnlyByPhpDoc(): bool + { + return $this->isReadOnlyByPhpDoc; + } + public function getReadableType(): Type { if ($this->type === null) { $this->type = TypehintHelper::decideTypeFromReflection( $this->nativeType, $this->phpDocType, - $this->declaringClass->getName() + $this->declaringClass, ); } @@ -111,15 +113,44 @@ 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 hasPhpDoc(): bool + public function isPromoted(): bool + { + return $this->reflection->isPromoted(); + } + + public function hasPhpDocType(): bool { return $this->phpDocType !== null; } @@ -133,13 +164,18 @@ public function getPhpDocType(): Type return new MixedType(); } + public function hasNativeType(): bool + { + return $this->nativeType !== null; + } + public function getNativeType(): Type { if ($this->finalNativeType === null) { $this->finalNativeType = TypehintHelper::decideTypeFromReflection( $this->nativeType, null, - $this->declaringClass->getName() + $this->declaringClass, ); } @@ -148,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 @@ -175,4 +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 098e62a820..29975a5e3f 100644 --- a/src/Reflection/Php/SimpleXMLElementProperty.php +++ b/src/Reflection/Php/SimpleXMLElementProperty.php @@ -3,29 +3,32 @@ 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 { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \PHPStan\Type\Type $type; - public function __construct( - ClassReflection $declaringClass, - Type $type + private string $name, + private ClassReflection $declaringClass, + private Type $type, ) { - $this->declaringClass = $declaringClass; - $this->type = $type; + } + + public function getName(): string + { + return $this->name; } public function getDeclaringClass(): ClassReflection @@ -48,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; @@ -60,7 +83,7 @@ public function getWritableType(): Type new IntegerType(), new FloatType(), new StringType(), - new BooleanType() + new BooleanType(), ); } @@ -99,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 new file mode 100644 index 0000000000..1017888c59 --- /dev/null +++ b/src/Reflection/Php/Soap/SoapClientMethodReflection.php @@ -0,0 +1,100 @@ +declaringClass; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getName(): string + { + return $this->name; + } + + public function getPrototype(): ClassMemberReflection + { + return $this; + } + + public function getVariants(): array + { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [], + true, + new MixedType(true), + ), + ]; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getThrowType(): Type + { + return new ObjectType('SoapFault'); + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + +} diff --git a/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php new file mode 100644 index 0000000000..431026f938 --- /dev/null +++ b/src/Reflection/Php/Soap/SoapClientMethodsClassReflectionExtension.php @@ -0,0 +1,22 @@ +is('SoapClient'); + } + + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + return new SoapClientMethodReflection($classReflection, $methodName); + } + +} diff --git a/src/Reflection/Php/UniversalObjectCrateProperty.php b/src/Reflection/Php/UniversalObjectCrateProperty.php index 5dbad67143..564613e219 100644 --- a/src/Reflection/Php/UniversalObjectCrateProperty.php +++ b/src/Reflection/Php/UniversalObjectCrateProperty.php @@ -3,27 +3,28 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; +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 \PHPStan\Reflection\PropertyReflection +final class UniversalObjectCrateProperty implements ExtendedPropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \PHPStan\Type\Type $readableType; - - private \PHPStan\Type\Type $writableType; - public function __construct( - ClassReflection $declaringClass, - Type $readableType, - Type $writableType + private string $name, + private ClassReflection $declaringClass, + private Type $readableType, + private Type $writableType, ) { - $this->declaringClass = $declaringClass; - $this->readableType = $readableType; - $this->writableType = $writableType; + } + + public function getName(): string + { + return $this->name; } public function getDeclaringClass(): ClassReflection @@ -46,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; @@ -91,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 8c0ec93652..0cd79eb232 100644 --- a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php +++ b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php @@ -2,54 +2,56 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Broker\Broker; +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 - implements \PHPStan\Reflection\PropertiesClassReflectionExtension, \PHPStan\Reflection\BrokerAwareExtension +final class UniversalObjectCratesClassReflectionExtension + implements PropertiesClassReflectionExtension { - /** @var string[] */ - private array $classes; - - private \PHPStan\Broker\Broker $broker; - /** - * @param string[] $classes + * @param list $classes */ - public function __construct(array $classes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private array $classes, + private AnnotationsPropertiesClassReflectionExtension $annotationClassReflection, + ) { - $this->classes = $classes; } - public function setBroker(Broker $broker): void + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - $this->broker = $broker; + return self::isUniversalObjectCrateImplementation( + $this->reflectionProvider, + $this->classes, + $classReflection, + ); } - public function hasProperty(ClassReflection $classReflection, string $propertyName): bool + public static function isUniversalObjectCrate( + ReflectionProvider $reflectionProvider, + ClassReflection $classReflection, + ): bool { - return self::isUniversalObjectCrate( - $this->broker, - $this->classes, - $classReflection + return self::isUniversalObjectCrateImplementation( + $reflectionProvider, + $reflectionProvider->getUniversalObjectCratesClasses(), + $classReflection, ); } /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param string[] $classes - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @return bool + * @param list $classes */ - public static function isUniversalObjectCrate( + private static function isUniversalObjectCrateImplementation( ReflectionProvider $reflectionProvider, array $classes, - ClassReflection $classReflection + ClassReflection $classReflection, ): bool { foreach ($classes as $className) { @@ -57,10 +59,7 @@ public static function isUniversalObjectCrate( continue; } - if ( - $classReflection->getName() === $className - || $classReflection->isSubclassOf($className) - ) { + if ($classReflection->is($className)) { return true; } } @@ -70,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 ea74d54df8..3b7387f85a 100644 --- a/src/Reflection/ReflectionProvider.php +++ b/src/Reflection/ReflectionProvider.php @@ -2,11 +2,14 @@ namespace PHPStan\Reflection; +use PhpParser\Node; use PHPStan\Analyser\Scope; +/** @api */ interface ReflectionProvider { + /** @phpstan-assert-if-true =class-string $className */ public function hasClass(string $className): bool; public function getClass(string $className): ClassReflection; @@ -14,20 +17,23 @@ public function getClass(string $className): ClassReflection; public function getClassName(string $className): string; public function getAnonymousClassReflection( - \PhpParser\Node\Stmt\Class_ $classNode, - Scope $scope + Node\Stmt\Class_ $classNode, + Scope $scope, ): ClassReflection; - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool; + /** @return list */ + public function getUniversalObjectCratesClasses(): array; - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection; + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string; + public function getFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): FunctionReflection; - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool; + public function resolveFunctionName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string; - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection; + public function hasConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool; - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string; + public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ConstantReflection; + + 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 a2fd9d99eb..0000000000 --- a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php +++ /dev/null @@ -1,155 +0,0 @@ -providers = $providers; - } - - public function hasClass(string $className): bool - { - foreach ($this->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 \PHPStan\Broker\ClassNotFoundException($className); - } - - public function getClassName(string $className): string - { - foreach ($this->providers as $provider) { - if (!$provider->hasClass($className)) { - continue; - } - - return $provider->getClassName($className); - } - - throw new \PHPStan\Broker\ClassNotFoundException($className); - } - - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - foreach ($this->providers as $provider) { - return $provider->getAnonymousClassReflection($classNode, $scope); - } - - throw new \PHPStan\ShouldNotHappenException(); - } - - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - foreach ($this->providers as $provider) { - if (!$provider->hasFunction($nameNode, $scope)) { - continue; - } - - return true; - } - - return false; - } - - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - foreach ($this->providers as $provider) { - if (!$provider->hasFunction($nameNode, $scope)) { - continue; - } - - return $provider->getFunction($nameNode, $scope); - } - - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); - } - - public function resolveFunctionName(\PhpParser\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(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - foreach ($this->providers as $provider) { - if (!$provider->hasConstant($nameNode, $scope)) { - continue; - } - - return true; - } - - return false; - } - - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - foreach ($this->providers as $provider) { - if (!$provider->hasConstant($nameNode, $scope)) { - continue; - } - - return $provider->getConstant($nameNode, $scope); - } - - throw new \PHPStan\Broker\ConstantNotFoundException((string) $nameNode); - } - - public function resolveConstantName(\PhpParser\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 8267abb13c..0000000000 --- a/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php +++ /dev/null @@ -1,113 +0,0 @@ -reflectionProvider = $reflectionProvider; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - $this->patterns = $patterns; - } - - public function hasClass(string $className): bool - { - if ($this->phpStormStubsSourceStubber->hasClass($className) && $className !== \Generator::class) { - // check that userland class isn't aliased to the same name as a class from stubs - if (!class_exists($className, false)) { - return false; - } - $reflection = new \ReflectionClass($className); - if ($reflection->getFileName() === false) { - return false; - } - } - - foreach ($this->patterns as $pattern) { - if (Strings::match($className, $pattern) !== null) { - return false; - } - } - - return $this->reflectionProvider->hasClass($className); - } - - public function getClass(string $className): ClassReflection - { - if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); - } - - return $this->reflectionProvider->getClass($className); - } - - public function getClassName(string $className): string - { - if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); - } - - return $this->reflectionProvider->getClassName($className); - } - - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection - { - return $this->reflectionProvider->getAnonymousClassReflection($classNode, $scope); - } - - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasFunction($nameNode, $scope); - } - - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $scope); - } - - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); - } - - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $scope); - } - - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $scope); - } - - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveConstantName($nameNode, $scope); - } - -} diff --git a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php index 96b4dd2fe0..2fccc5b3c4 100644 --- a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php @@ -4,14 +4,11 @@ use PHPStan\Reflection\ReflectionProvider; -class DirectReflectionProviderProvider implements ReflectionProviderProvider +final class DirectReflectionProviderProvider implements ReflectionProviderProvider { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getReflectionProvider(): ReflectionProvider diff --git a/src/Reflection/ReflectionProvider/DummyReflectionProvider.php b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php new file mode 100644 index 0000000000..7d18639f8c --- /dev/null +++ b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php @@ -0,0 +1,72 @@ +container = $container; } public function getReflectionProvider(): ReflectionProvider diff --git a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php index 3610a2af82..00f4301c7e 100644 --- a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php @@ -2,39 +2,38 @@ namespace PHPStan\Reflection\ReflectionProvider; +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 { - private \PHPStan\Reflection\ReflectionProvider $provider; - /** @var array */ private array $hasClasses = []; - /** @var array */ + /** @var array */ private array $classes = []; /** @var array */ private array $classNames = []; - public function __construct(ReflectionProvider $provider) + public function __construct(private ReflectionProvider $provider) { - $this->provider = $provider; } public function hasClass(string $className): bool { - $lowerClassName = strtolower($className); - if (isset($this->hasClasses[$lowerClassName])) { - return $this->hasClasses[$lowerClassName]; + if (isset($this->hasClasses[$className])) { + return $this->hasClasses[$className]; } - return $this->hasClasses[$lowerClassName] = $this->provider->hasClass($className); + return $this->hasClasses[$className] = $this->provider->hasClass($className); } public function getClass(string $className): ClassReflection @@ -57,39 +56,44 @@ public function getClassName(string $className): string return $this->classNames[$lowerClassName] = $this->provider->getClassName($className); } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { return $this->provider->getAnonymousClassReflection($classNode, $scope); } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function getUniversalObjectCratesClasses(): array + { + return $this->provider->getUniversalObjectCratesClasses(); + } + + public function hasFunction(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): bool { - return $this->provider->hasFunction($nameNode, $scope); + return $this->provider->hasFunction($nameNode, $namespaceAnswerer); } - public function getFunction(\PhpParser\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(\PhpParser\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(\PhpParser\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(\PhpParser\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(\PhpParser\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 cc0d3cb35e..06441ef778 100644 --- a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php +++ b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php @@ -4,37 +4,18 @@ use PHPStan\Reflection\ReflectionProvider; -class ReflectionProviderFactory +final class ReflectionProviderFactory { - private \PHPStan\Reflection\ReflectionProvider $runtimeReflectionProvider; - - private \PHPStan\Reflection\ReflectionProvider $staticReflectionProvider; - - private bool $disableRuntimeReflectionProvider; - public function __construct( - ReflectionProvider $runtimeReflectionProvider, - ReflectionProvider $staticReflectionProvider, - bool $disableRuntimeReflectionProvider + private ReflectionProvider $staticReflectionProvider, ) { - $this->runtimeReflectionProvider = $runtimeReflectionProvider; - $this->staticReflectionProvider = $staticReflectionProvider; - $this->disableRuntimeReflectionProvider = $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 new file mode 100644 index 0000000000..90ad0dd185 --- /dev/null +++ b/src/Reflection/ReflectionProviderStaticAccessor.php @@ -0,0 +1,29 @@ +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 69fdabbb43..5b5cb6b4e6 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -2,83 +2,15 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\DummyParameter; -use PHPStan\Type\Generic\TemplateTypeHelper; -use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; -class ResolvedFunctionVariant implements ParametersAcceptor +interface ResolvedFunctionVariant extends ExtendedParametersAcceptor { - private ParametersAcceptor $parametersAcceptor; + public function getOriginalParametersAcceptor(): ParametersAcceptor; - private TemplateTypeMap $resolvedTemplateTypeMap; + public function getReturnTypeWithUnresolvableTemplateTypes(): Type; - /** @var ParameterReflection[]|null */ - private ?array $parameters = null; - - private ?Type $returnType = null; - - public function __construct( - ParametersAcceptor $parametersAcceptor, - TemplateTypeMap $resolvedTemplateTypeMap - ) - { - $this->parametersAcceptor = $parametersAcceptor; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map(function (ParameterReflection $param): ParameterReflection { - return new DummyParameter( - $param->getName(), - TemplateTypeHelper::resolveTemplateTypes($param->getType(), $this->resolvedTemplateTypeMap), - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue() - ); - }, $this->parametersAcceptor->getParameters()); - - $this->parameters = $parameters; - } - - return $parameters; - } - - public function isVariadic(): bool - { - return $this->parametersAcceptor->isVariadic(); - } - - public function getReturnType(): Type - { - $type = $this->returnType; - - if ($type === null) { - $type = TemplateTypeHelper::resolveTemplateTypes( - $this->parametersAcceptor->getReturnType(), - $this->resolvedTemplateTypeMap - ); - - $this->returnType = $type; - } - - return $type; - } + 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 77e03b7f4d..134e566eca 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -4,23 +4,32 @@ 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 { - private MethodReflection $reflection; + /** @var list|null */ + private ?array $variants = null; - private TemplateTypeMap $resolvedTemplateTypeMap; + /** @var list|null */ + private ?array $namedArgumentVariants = null; - /** @var \PHPStan\Reflection\ParametersAcceptor[]|null */ - private ?array $variants = null; + private ?Assertions $asserts = null; - public function __construct(MethodReflection $reflection, TemplateTypeMap $resolvedTemplateTypeMap) + private Type|false|null $selfOutType = false; + + public function __construct( + private ExtendedMethodReflection $reflection, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) { - $this->reflection = $reflection; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; } public function getName(): string @@ -33,9 +42,6 @@ public function getPrototype(): ClassMemberReflection return $this->reflection->getPrototype(); } - /** - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getVariants(): array { $variants = $this->variants; @@ -43,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->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + [], ); } - $this->variants = $variants; - - return $variants; + return $result; } public function getDeclaringClass(): ClassReflection @@ -105,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(); @@ -120,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 335a16fb5c..df7a33f84d 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -6,26 +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 PropertyReflection +final class ResolvedPropertyReflection implements WrapperPropertyReflection { - private PropertyReflection $reflection; - - private TemplateTypeMap $templateTypeMap; - private ?Type $readableType = null; private ?Type $writableType = null; - public function __construct(PropertyReflection $reflection, TemplateTypeMap $templateTypeMap) + public function __construct( + private ExtendedPropertyReflection $reflection, + private TemplateTypeMap $templateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) + { + } + + public function getName(): string { - $this->reflection = $reflection; - $this->templateTypeMap = $templateTypeMap; + return $this->reflection->getName(); } - public function getOriginalReflection(): PropertyReflection + public function getOriginalReflection(): ExtendedPropertyReflection { return $this->reflection; } @@ -59,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; @@ -68,7 +93,15 @@ public function getReadableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getReadableType(), - $this->templateTypeMap + $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + $type = TemplateTypeHelper::resolveTemplateTypes( + $type, + $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); $this->readableType = $type; @@ -85,7 +118,15 @@ public function getWritableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getWritableType(), - $this->templateTypeMap + $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ); + $type = TemplateTypeHelper::resolveTemplateTypes( + $type, + $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), ); $this->writableType = $type; @@ -128,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 917b2f7822..0000000000 --- a/src/Reflection/Runtime/RuntimeReflectionProvider.php +++ /dev/null @@ -1,428 +0,0 @@ -reflectionProviderProvider = $reflectionProviderProvider; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->functionReflectionFactory = $functionReflectionFactory; - $this->fileTypeMapper = $fileTypeMapper; - $this->nativeFunctionReflectionProvider = $nativeFunctionReflectionProvider; - $this->printer = $printer; - $this->anonymousClassNameHelper = $anonymousClassNameHelper; - $this->fileHelper = $fileHelper; - $this->relativePathHelper = $relativePathHelper; - $this->stubPhpDocProvider = $stubPhpDocProvider; - } - - public function getClass(string $className): \PHPStan\Reflection\ClassReflection - { - /** @var class-string $className */ - $className = $className; - if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\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 \PHPStan\Broker\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 getAnonymousClassReflection( - \PhpParser\Node\Stmt\Class_ $classNode, - Scope $scope - ): ClassReflection - { - if (isset($classNode->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if (!$scope->isInTrait()) { - $scopeFile = $scope->getFile(); - } else { - $scopeFile = $scope->getTraitReflection()->getFileName(); - if ($scopeFile === false) { - $scopeFile = $scope->getFile(); - } - } - - $filename = $this->fileHelper->normalizePath($this->relativePathHelper->getRelativePath($scopeFile), '/'); - - $className = $this->anonymousClassNameHelper->getAnonymousClassName( - $classNode, - $scopeFile - ); - $classNode->name = new \PhpParser\Node\Identifier($className); - $classNode->setAttribute('anonymousClass', true); - - if (isset(self::$anonymousClasses[$className])) { - return self::$anonymousClasses[$className]; - } - - eval($this->printer->prettyPrint([$classNode])); - - /** @var class-string $className */ - $className = $className; - - self::$anonymousClasses[$className] = $this->getClassFromReflection( - new \ReflectionClass($className), - sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()), - $scopeFile - ); - $this->classReflections[$className] = self::$anonymousClasses[$className]; - - return self::$anonymousClasses[$className]; - } - - /** - * @param \ReflectionClass $reflectionClass - * @param string $displayName - * @param string|null $anonymousFilename - */ - 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->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 - { - $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 \PHPStan\Broker\ClassAutoloadingException($autoloadedClassName); - } - }); - - try { - return $this->hasClassCache[$className] = class_exists($className) || interface_exists($className) || trait_exists($className); - } catch (\PHPStan\Broker\ClassAutoloadingException $e) { - throw $e; - } catch (\Throwable $t) { - throw new \PHPStan\Broker\ClassAutoloadingException( - $className, - $t - ); - } finally { - spl_autoload_unregister($autoloader); - } - } - - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): \PHPStan\Reflection\FunctionReflection - { - $functionName = $this->resolveFunctionName($nameNode, $scope); - if ($functionName === null) { - throw new \PHPStan\Broker\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(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->resolveFunctionName($nameNode, $scope) !== null; - } - - private function hasCustomFunction(\PhpParser\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(\PhpParser\Node\Name $nameNode, ?Scope $scope): \PHPStan\Reflection\Php\PhpFunctionReflection - { - if (!$this->hasCustomFunction($nameNode, $scope)) { - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); - } - - /** @var string $functionName */ - $functionName = $this->resolveFunctionName($nameNode, $scope); - if (!function_exists($functionName)) { - throw new \PHPStan\Broker\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; - $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName()); - 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(); - } - - $functionReflection = $this->functionReflectionFactory->create( - $reflectionFunction, - $templateTypeMap, - array_map(static function (ParamTag $paramTag): Type { - return $paramTag->getType(); - }, $phpDocParameterTags), - $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, - $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, - $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, - $isDeprecated, - $isInternal, - $isFinal, - $reflectionFunction->getFileName() - ); - $this->customFunctionReflections[$lowerCasedFunctionName] = $functionReflection; - - return $functionReflection; - } - - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->resolveName($nameNode, static function (string $name): bool { - $exists = function_exists($name); - if ($exists) { - return true; - } - - return false; - }, $scope); - } - - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->resolveConstantName($nameNode, $scope) !== null; - } - - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - $constantName = $this->resolveConstantName($nameNode, $scope); - if ($constantName === null) { - throw new \PHPStan\Broker\ConstantNotFoundException((string) $nameNode); - } - - return new RuntimeConstantReflection( - $constantName, - ConstantTypeHelper::getTypeFromValue(constant($constantName)), - null - ); - } - - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->resolveName($nameNode, static function (string $name): bool { - return defined($name); - }, $scope); - } - - /** - * @param Node\Name $nameNode - * @param \Closure(string $name): bool $existsCallback - * @param Scope|null $scope - * @return string|null - */ - private function resolveName( - \PhpParser\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($traceStep['function']) - && 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 b9e197163c..f9107d4b23 100644 --- a/src/Reflection/SignatureMap/FunctionSignature.php +++ b/src/Reflection/SignatureMap/FunctionSignature.php @@ -4,34 +4,23 @@ use PHPStan\Type\Type; -class FunctionSignature +final class FunctionSignature { - /** @var \PHPStan\Reflection\SignatureMap\ParameterSignature[] */ - private array $parameters; - - private \PHPStan\Type\Type $returnType; - - private bool $variadic; - /** - * @param array $parameters - * @param \PHPStan\Type\Type $returnType - * @param bool $variadic + * @param list $parameters */ public function __construct( - array $parameters, - Type $returnType, - bool $variadic + private array $parameters, + private Type $returnType, + private Type $nativeReturnType, + private bool $variadic, ) { - $this->parameters = $parameters; - $this->returnType = $returnType; - $this->variadic = $variadic; } /** - * @return array + * @return list */ public function getParameters(): array { @@ -43,6 +32,11 @@ public function getReturnType(): Type return $this->returnType; } + public function getNativeReturnType(): Type + { + return $this->nativeReturnType; + } + public function isVariadic(): bool { return $this->variadic; diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php new file mode 100644 index 0000000000..7f58e257fe --- /dev/null +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -0,0 +1,262 @@ + */ + private static array $signatureMaps = []; + + /** @var array|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 hasFunctionSignature(string $name): bool + { + return array_key_exists(strtolower($name), $this->getSignatureMap()); + } + + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array + { + return $this->getFunctionSignatures(sprintf('%s::%s', $className, $methodName), $className, $reflectionMethod); + } + + public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array + { + $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 ['positional' => $signatures, 'named' => null]; + } + + private function createSignature(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): FunctionSignature + { + 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 ($reflectionFunction === null) { + $parameters[] = $parameter; + continue; + } + $nativeParameters = $reflectionFunction->getParameters(); + if (!array_key_exists($i, $nativeParameters)) { + $parameters[] = $parameter; + continue; + } + + $parameters[] = new ParameterSignature( + $parameter->getName(), + $parameter->isOptional(), + $parameter->getType(), + 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 ($reflectionFunction === null) { + $nativeReturnType = new MixedType(); + } else { + $nativeReturnType = TypehintHelper::decideTypeFromReflection($reflectionFunction->getReturnType()); + } + + return new FunctionSignature( + $parameters, + $signature->getReturnType(), + $nativeReturnType, + $signature->isVariadic(), + ); + } + + public function hasMethodMetadata(string $className, string $methodName): bool + { + return $this->hasFunctionMetadata(sprintf('%s::%s', $className, $methodName)); + } + + public function hasFunctionMetadata(string $name): bool + { + $signatureMap = self::getFunctionMetadataMap(); + return array_key_exists(strtolower($name), $signatureMap); + } + + /** + * @return array{hasSideEffects: bool} + */ + public function getMethodMetadata(string $className, string $methodName): array + { + return $this->getFunctionMetadata(sprintf('%s::%s', $className, $methodName)); + } + + /** + * @return array{hasSideEffects: bool} + */ + public function getFunctionMetadata(string $functionName): array + { + $functionName = strtolower($functionName); + + if (!$this->hasFunctionMetadata($functionName)) { + throw new ShouldNotHappenException(); + } + + return self::getFunctionMetadataMap()[$functionName]; + } + + /** + * @return array + */ + private static function getFunctionMetadataMap(): array + { + if (self::$functionMetadata === null) { + /** @var array $metadata */ + $metadata = require __DIR__ . '/../../../resources/functionMetadata.php'; + self::$functionMetadata = array_change_key_case($metadata, CASE_LOWER); + } + + return self::$functionMetadata; + } + + /** + * @return mixed[] + */ + public function getSignatureMap(): array + { + $cacheKey = sprintf('%d-%d', $this->phpVersion->getVersionId(), $this->stricterFunctionMap ? 1 : 0); + if (array_key_exists($cacheKey, self::$signatureMaps)) { + return self::$signatureMaps[$cacheKey]; + } + + $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; + if (!is_array($signatureMap)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); + } + + $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); + + if ($this->stricterFunctionMap) { + $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) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php80delta.php'); + + if ($this->stricterFunctionMap) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php80delta_bleedingEdge.php'); + } + } + + if ($this->phpVersion->getVersionId() >= 80100) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php81delta.php'); + } + + 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); + } + + /** + * @param array $signatureMap + * @param array> $delta + * @return array + */ + private function computeSignatureMap(array $signatureMap, array $delta): array + { + foreach (array_keys($delta['old']) as $key) { + unset($signatureMap[strtolower($key)]); + } + foreach ($delta['new'] as $key => $signature) { + $signatureMap[strtolower($key)] = $signature; + } + + 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 2114db297c..766e665115 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -2,87 +2,148 @@ namespace PHPStan\Reflection\SignatureMap; -use PHPStan\Reflection\FunctionVariant; +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\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\BooleanType; -use PHPStan\Type\FloatType; +use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\IntegerType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; +use PHPStan\Type\MixedType; +use PHPStan\Type\Type; +use PHPStan\Type\TypehintHelper; +use function array_key_exists; +use function array_map; +use function strtolower; -class NativeFunctionReflectionProvider +final class NativeFunctionReflectionProvider { /** @var NativeFunctionReflection[] */ - private static array $functionMap = []; + private array $functionMap = []; - private \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider; - - public function __construct(SignatureMapProvider $signatureMapProvider) + public function __construct( + private SignatureMapProvider $signatureMapProvider, + private Reflector $reflector, + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, + private AttributeReflectionFactory $attributeReflectionFactory, + ) { - $this->signatureMapProvider = $signatureMapProvider; } public function findFunctionReflection(string $functionName): ?NativeFunctionReflection { $lowerCasedFunctionName = strtolower($functionName); - if (isset(self::$functionMap[$lowerCasedFunctionName])) { - return self::$functionMap[$lowerCasedFunctionName]; + $realFunctionName = $lowerCasedFunctionName; + if (isset($this->functionMap[$lowerCasedFunctionName])) { + return $this->functionMap[$lowerCasedFunctionName]; } if (!$this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName)) { return null; } - $variantName = $lowerCasedFunctionName; - $variants = []; - $i = 0; - while ($this->signatureMapProvider->hasFunctionSignature($variantName)) { - $functionSignature = $this->signatureMapProvider->getFunctionSignature($variantName, null); - $returnType = $functionSignature->getReturnType(); - if ($lowerCasedFunctionName === 'pow') { - $returnType = TypeUtils::toBenevolentUnion($returnType); - } - $variants[] = new FunctionVariant( - TemplateTypeMap::createEmpty(), - null, - array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName): NativeParameterReflection { - $type = $parameterSignature->getType(); - if ( - $parameterSignature->getName() === 'args' - && ( - $lowerCasedFunctionName === 'printf' - || $lowerCasedFunctionName === 'sprintf' - ) - ) { - $type = new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]); + $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(); + if ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); + $throwsTag = $resolvedPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); } - return new NativeParameterReflection( - $parameterSignature->getName(), - $parameterSignature->isOptional(), - $type, - $parameterSignature->passedByReference(), - $parameterSignature->isVariadic(), - null - ); - }, $functionSignature->getParameters()), - $functionSignature->isVariadic(), - $returnType - ); - - $i++; - $variantName = sprintf($lowerCasedFunctionName . '\'' . $i); + } + } + } 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)) { @@ -90,15 +151,44 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } else { $hasSideEffects = TrinaryLogic::createMaybe(); } + $functionReflection = new NativeFunctionReflection( - $lowerCasedFunctionName, - $variants, - null, - $hasSideEffects + $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; } + private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type + { + $returnTag = $phpDoc->getReturnTag(); + if ($returnTag === null) { + return null; + } + + 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 9db2acb685..7649825119 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -5,32 +5,20 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; -class ParameterSignature +final class ParameterSignature { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - public function __construct( - string $name, - bool $optional, - Type $type, - PassedByReference $passedByReference, - bool $variadic + private string $name, + private bool $optional, + private Type $type, + private Type $nativeType, + private PassedByReference $passedByReference, + private bool $variadic, + private ?Type $defaultValue, + private ?Type $outType, ) { - $this->name = $name; - $this->optional = $optional; - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; } public function getName(): string @@ -48,6 +36,11 @@ public function getType(): Type return $this->type; } + public function getNativeType(): Type + { + return $this->nativeType; + } + public function passedByReference(): PassedByReference { return $this->passedByReference; @@ -58,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 new file mode 100644 index 0000000000..b787a4201b --- /dev/null +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -0,0 +1,526 @@ +> */ + 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): bool + { + $lowerClassName = strtolower($className); + if ($lowerClassName === 'backedenum') { + return false; + } + 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); + } + + return true; + } + + /** + * @return array{ClassMethod, string}|null + */ + private function findMethodNode(string $className, string $methodName): ?array + { + $lowerClassName = strtolower($className); + $lowerMethodName = strtolower($methodName); + if (isset($this->methodNodes[$lowerClassName][$lowerMethodName])) { + return $this->methodNodes[$lowerClassName][$lowerMethodName]; + } + + $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 ClassMethod) { + continue; + } + + if ($stmt->name->toLowerString() === $lowerMethodName) { + if (!$this->isForCurrentVersion($stmt->attrGroups)) { + continue; + } + return $this->methodNodes[$lowerClassName][$lowerMethodName] = [$stmt, $stubFile]; + } + } + + return null; + } + + /** + * @param AttributeGroup[] $attrGroups + */ + private function isForCurrentVersion(array $attrGroups): bool + { + 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 hasFunctionSignature(string $name): bool + { + $lowerName = strtolower($name); + if (!array_key_exists($lowerName, $this->map->functions)) { + return $this->functionSignatureMapProvider->hasFunctionSignature($name); + } + + return true; + } + + 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->getMethodSignatures($className, $methodName, $reflectionMethod); + } + + [$methodNode, $stubFile] = $methodNode; + + $signature = $this->getSignature($methodNode, $className, $stubFile); + if ($this->functionSignatureMapProvider->hasMethodSignature($className, $methodName)) { + $functionMapSignatures = $this->functionSignatureMapProvider->getMethodSignatures($className, $methodName, $reflectionMethod); + + return $this->getMergedSignatures($signature, $functionMapSignatures); + } + + return ['positional' => [$signature], 'named' => null]; + } + + public function getFunctionSignatures(string $functionName, ?string $className, ReflectionFunctionAbstract|null $reflectionFunction): array + { + $lowerName = strtolower($functionName); + if (!array_key_exists($lowerName, $this->map->functions)) { + return $this->functionSignatureMapProvider->getFunctionSignatures($functionName, $className, $reflectionFunction); + } + + $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]; + } + + 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]; + } + + if (count($functionMapSignatures['positional']) === 0) { + return ['positional' => [], 'named' => null]; + } + + $nativeParams = $nativeSignature->getParameters(); + $namedArgumentsVariants = []; + $allParamNamesMatchNative = true; + foreach ($functionMapSignatures['positional'] as $functionMapSignature) { + $isPrevParamVariadic = false; + $hasMiddleVariadicParam = false; + // avoid weird functions like array_diff_uassoc + foreach ($functionMapSignature->getParameters() as $i => $functionParam) { + $nativeParam = $nativeParams[$i] ?? null; + $allParamNamesMatchNative = $allParamNamesMatchNative && $nativeParam !== null && $functionParam->getName() === $nativeParam->getName(); + $hasMiddleVariadicParam = $hasMiddleVariadicParam || $isPrevParamVariadic; + $isPrevParamVariadic = $functionParam->isVariadic() || ( + $nativeParam !== null + ? $nativeParam->isVariadic() + : false + ); + } + + if ($hasMiddleVariadicParam) { + continue; + } + + $parameters = []; + foreach ($functionMapSignature->getParameters() as $i => $functionParam) { + if (!array_key_exists($i, $nativeParams)) { + continue 2; + } + + // it seems that variadic parameters cannot be named in native functions/methods. + $nativeParam = $nativeParams[$i]; + if ($nativeParam->isVariadic()) { + break; + } + + $parameters[] = new ParameterSignature( + $nativeParam->getName(), + $functionParam->isOptional(), + $functionParam->getType(), + $functionParam->getNativeType(), + $functionParam->passedByReference(), + $functionParam->isVariadic(), + $functionParam->getDefaultValue() ?? $nativeParam->getDefaultValue(), + $functionParam->getOutType() ?? $nativeParam->getOutType(), + ); + } + + $namedArgumentsVariants[] = new FunctionSignature( + $parameters, + $functionMapSignature->getReturnType(), + $functionMapSignature->getNativeReturnType(), + $functionMapSignature->isVariadic(), + ); + } + + if ($allParamNamesMatchNative || count($namedArgumentsVariants) === 0) { + $namedArgumentsVariants = null; + } + + return ['positional' => $functionMapSignatures['positional'], 'named' => $namedArgumentsVariants]; + } + + private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSignature $functionMapSignature): FunctionSignature + { + $parameters = []; + foreach ($nativeSignature->getParameters() as $i => $nativeParameter) { + if (!array_key_exists($i, $functionMapSignature->getParameters())) { + $parameters[] = $nativeParameter; + continue; + } + + $functionMapParameter = $functionMapSignature->getParameters()[$i]; + $nativeParameterType = $nativeParameter->getNativeType(); + $parameters[] = new ParameterSignature( + $nativeParameter->getName(), + $nativeParameter->isOptional(), + TypehintHelper::decideType( + $nativeParameterType, + TypehintHelper::decideType( + $nativeParameter->getType(), + $functionMapParameter->getType(), + ), + ), + $nativeParameterType, + $nativeParameter->passedByReference()->yes() ? $functionMapParameter->passedByReference() : $nativeParameter->passedByReference(), + $nativeParameter->isVariadic(), + $nativeParameter->getDefaultValue(), + $nativeParameter->getOutType(), + ); + } + + $nativeReturnType = $nativeSignature->getNativeReturnType(); + if ($nativeReturnType instanceof MixedType && !$nativeReturnType->isExplicitMixed()) { + $returnType = $functionMapSignature->getReturnType(); + } else { + $returnType = TypehintHelper::decideType( + $nativeReturnType, + TypehintHelper::decideType( + $nativeSignature->getReturnType(), + $functionMapSignature->getReturnType(), + ), + ); + } + + return new FunctionSignature( + $parameters, + $returnType, + $nativeReturnType, + $nativeSignature->isVariadic(), + ); + } + + public function hasMethodMetadata(string $className, string $methodName): bool + { + return $this->functionSignatureMapProvider->hasMethodMetadata($className, $methodName); + } + + public function hasFunctionMetadata(string $name): bool + { + return $this->functionSignatureMapProvider->hasFunctionMetadata($name); + } + + /** + * @return array{hasSideEffects: bool} + */ + public function getMethodMetadata(string $className, string $methodName): array + { + return $this->functionSignatureMapProvider->getMethodMetadata($className, $methodName); + } + + /** + * @return array{hasSideEffects: bool} + */ + public function getFunctionMetadata(string $functionName): array + { + return $this->functionSignatureMapProvider->getFunctionMetadata($functionName); + } + + private function getSignature( + ClassMethod|Function_ $function, + ?string $className, + string $stubFile, + ): FunctionSignature + { + $phpDocParameterTypes = null; + $phpDocReturnType = null; + if ($function->getDocComment() !== null) { + if ($function instanceof ClassMethod) { + $functionName = $function->name->toString(); + } elseif ($function->namespacedName !== null) { + $functionName = $function->namespacedName->toString(); + } else { + throw new ShouldNotHappenException(); + } + $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $stubFile, + $className, + null, + $functionName, + $function->getDocComment()->getText(), + ); + $phpDocParameterTypes = array_map(static fn (ParamTag $param): Type => $param->getType(), $phpDoc->getParamTags()); + if ($phpDoc->getReturnTag() !== null) { + $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) { + $name = $param->var; + if (!$name instanceof Variable || !is_string($name->name)) { + throw new ShouldNotHappenException(); + } + $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, $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(), $classReflection); + + return new FunctionSignature( + $parameters, + TypehintHelper::decideType($returnType, $phpDocReturnType ?? null), + $returnType, + $variadic, + ); + } + + 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 67b9985182..e60cede66d 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -2,19 +2,24 @@ namespace PHPStan\Reflection\SignatureMap; +use Nette\Utils\Strings; use PHPStan\Analyser\NameScope; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function array_slice; +use function str_starts_with; +use function substr; -class SignatureMapParser +final class SignatureMapParser { - private \PHPStan\PhpDoc\TypeStringResolver $typeStringResolver; + private TypeStringResolver $typeStringResolver; public function __construct( - TypeStringResolver $typeNodeResolver + TypeStringResolver $typeNodeResolver, ) { $this->typeStringResolver = $typeNodeResolver; @@ -22,8 +27,6 @@ public function __construct( /** * @param mixed[] $map - * @param string|null $className - * @return \PHPStan\Reflection\SignatureMap\FunctionSignature */ public function getFunctionSignature(array $map, ?string $className): FunctionSignature { @@ -38,7 +41,8 @@ public function getFunctionSignature(array $map, ?string $className): FunctionSi return new FunctionSignature( $parameterSignatures, $this->getTypeFromString($map[0], $className), - $hasVariadic + new MixedType(), + $hasVariadic, ); } @@ -53,7 +57,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type /** * @param array $parameterMap - * @return array + * @return list */ private function getParameters(array $parameterMap): array { @@ -64,8 +68,11 @@ private function getParameters(array $parameterMap): array $name, $isOptional, $this->getTypeFromString($typeString, null), + new MixedType(), $passedByReference, - $isVariadic + $isVariadic, + null, + null, ); } @@ -73,29 +80,28 @@ private function getParameters(array $parameterMap): array } /** - * @param string $parameterNameString * @return mixed[] */ private function getParameterInfoFromName(string $parameterNameString): array { - $matches = \Nette\Utils\Strings::match( + $matches = Strings::match( $parameterNameString, - '#^(?P&(?:\.\.\.)?r?w?_?)?(?P\.\.\.)?(?P[^=]+)?(?P=)?($)#' + '#^(?P&(?:\.\.\.)?r?w?_?)?(?P\.\.\.)?(?P[^=]+)?(?P=)?($)#', ); if ($matches === null || !isset($matches['optional'])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $isVariadic = $matches['variadic'] !== ''; $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) { + } 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 4dddaa5350..f7ec5ed5ce 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -2,122 +2,42 @@ namespace PHPStan\Reflection\SignatureMap; -class SignatureMapProvider -{ - - private \PHPStan\Reflection\SignatureMap\SignatureMapParser $parser; - - /** @var mixed[]|null */ - private static ?array $signatureMap = null; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\Type\Type; +use ReflectionFunctionAbstract; - /** @var array|null */ - private static ?array $functionMetadata = null; - - public function __construct(SignatureMapParser $parser) - { - $this->parser = $parser; - } +interface SignatureMapProvider +{ - public function hasFunctionSignature(string $name): bool - { - $signatureMap = self::getSignatureMap(); - return array_key_exists(strtolower($name), $signatureMap); - } + public function hasMethodSignature(string $className, string $methodName): bool; - public function getFunctionSignature(string $functionName, ?string $className): FunctionSignature - { - $functionName = strtolower($functionName); + public function hasFunctionSignature(string $name): bool; - if (!$this->hasFunctionSignature($functionName)) { - throw new \PHPStan\ShouldNotHappenException(); - } + /** @return array{positional: array, named: ?array} */ + public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array; - $signatureMap = self::getSignatureMap(); + /** @return array{positional: array, named: ?array} */ + public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array; - return $this->parser->getFunctionSignature( - $signatureMap[$functionName], - $className - ); - } + public function hasMethodMetadata(string $className, string $methodName): bool; - public function hasFunctionMetadata(string $name): bool - { - $signatureMap = self::getFunctionMetadataMap(); - return array_key_exists(strtolower($name), $signatureMap); - } + public function hasFunctionMetadata(string $name): bool; /** - * @param string $functionName * @return array{hasSideEffects: bool} */ - public function getFunctionMetadata(string $functionName): array - { - $functionName = strtolower($functionName); - - if (!$this->hasFunctionMetadata($functionName)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return self::getFunctionMetadataMap()[$functionName]; - } + public function getMethodMetadata(string $className, string $methodName): array; /** - * @return array - */ - private static function getFunctionMetadataMap(): array - { - if (self::$functionMetadata === null) { - /** @var array $metadata */ - $metadata = require __DIR__ . '/functionMetadata.php'; - self::$functionMetadata = array_change_key_case($metadata, CASE_LOWER); - } - - return self::$functionMetadata; - } - - /** - * @return mixed[] + * @return array{hasSideEffects: bool} */ - private static function getSignatureMap(): array - { - if (self::$signatureMap === null) { - $signatureMap = require __DIR__ . '/functionMap.php'; - if (!is_array($signatureMap)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); + public function getFunctionMetadata(string $functionName): array; - if (PHP_VERSION_ID >= 70400) { - $php74MapDelta = require __DIR__ . '/functionMap_php74delta.php'; - if (!is_array($php74MapDelta)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = self::computeSignatureMap($signatureMap, $php74MapDelta); - } - - self::$signatureMap = $signatureMap; - } - - return self::$signatureMap; - } + public function hasClassConstantMetadata(string $className, string $constantName): bool; /** - * @param array $signatureMap - * @param array> $delta - * @return array + * @return array{nativeType: Type} */ - private static function computeSignatureMap(array $signatureMap, array $delta): array - { - foreach ($delta['old'] as $key) { - unset($signatureMap[strtolower($key)]); - } - foreach ($delta['new'] as $key => $signature) { - $signatureMap[strtolower($key)] = $signature; - } - - return $signatureMap; - } + public function getClassConstantMetadata(string $className, string $constantName): array; } diff --git a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php new file mode 100644 index 0000000000..4aa6d510da --- /dev/null +++ b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php @@ -0,0 +1,27 @@ +phpVersion->getVersionId() < 80000) { + return $this->functionSignatureMapProvider; + } + + return $this->php8SignatureMapProvider; + } + +} diff --git a/src/Reflection/SignatureMap/functionMap_php74delta.php b/src/Reflection/SignatureMap/functionMap_php74delta.php deleted file mode 100644 index 349cfe2337..0000000000 --- a/src/Reflection/SignatureMap/functionMap_php74delta.php +++ /dev/null @@ -1,60 +0,0 @@ - [ - '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::cdef' => ['FFI', 'code='=>'string', 'lib='=>'?string'], - '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::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::type' => ['FFI\CType', 'type'=>'string'], - 'get_mangled_object_vars' => ['array', 'obj'=>'object'], - 'mb_str_split' => ['array|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], - 'password_algos' => ['array'], - 'password_hash' => ['string|false', 'password'=>'string', 'algo'=>'string|null', 'options='=>'array'], - 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'string|null', 'options='=>'array'], - 'sapi_windows_set_ctrl_handler' => ['bool', 'callable'=>'callable', 'add='=>'bool'], - 'ReflectionProperty::getType' => ['?ReflectionType'], - 'ReflectionProperty::hasType' => ['bool'], - 'ReflectionProperty::isInitialized' => ['bool', 'object='=>'?object'], - 'ReflectionReference::fromArrayElement' => ['?ReflectionReference', 'array'=>'array', 'key'=>'int|string'], - 'ReflectionReference::getId' => ['string'], - 'SQLite3Stmt::getSQL' => ['string', 'expanded='=>'bool'], - 'strip_tags' => ['string', 'str'=>'string', 'allowable_tags='=>'string|array'], - 'WeakReference::create' => ['WeakReference', 'referent'=>'object'], - 'WeakReference::get' => ['?object'], - ], - 'old' => [ - 'implode\'2', - ], -]; diff --git a/src/Reflection/SignatureMap/functionMetadata.php b/src/Reflection/SignatureMap/functionMetadata.php deleted file mode 100644 index 2f96e528e4..0000000000 --- a/src/Reflection/SignatureMap/functionMetadata.php +++ /dev/null @@ -1,101 +0,0 @@ - ['hasSideEffects' => false], - 'acos' => ['hasSideEffects' => false], - 'acosh' => ['hasSideEffects' => false], - 'addcslashes' => ['hasSideEffects' => false], - 'addslashes' => ['hasSideEffects' => false], - 'array_change_key_case' => ['hasSideEffects' => false], - 'array_chunk' => ['hasSideEffects' => false], - 'array_column' => ['hasSideEffects' => false], - 'array_combine' => ['hasSideEffects' => false], - 'array_count_values' => ['hasSideEffects' => false], - 'array_diff' => ['hasSideEffects' => false], - 'array_diff_assoc' => ['hasSideEffects' => false], - 'array_diff_key' => ['hasSideEffects' => false], - 'array_diff_uassoc' => ['hasSideEffects' => false], - 'array_diff_ukey' => ['hasSideEffects' => false], - 'array_fill' => ['hasSideEffects' => false], - 'array_fill_keys' => ['hasSideEffects' => false], - 'array_flip' => ['hasSideEffects' => false], - 'array_intersect' => ['hasSideEffects' => false], - 'array_intersect_assoc' => ['hasSideEffects' => false], - 'array_intersect_key' => ['hasSideEffects' => false], - 'array_intersect_uassoc' => ['hasSideEffects' => false], - 'array_intersect_ukey' => ['hasSideEffects' => false], - 'array_key_first' => ['hasSideEffects' => false], - 'array_key_last' => ['hasSideEffects' => false], - 'array_key_exists' => ['hasSideEffects' => false], - 'array_keys' => ['hasSideEffects' => false], - 'array_merge' => ['hasSideEffects' => false], - 'array_merge_recursive' => ['hasSideEffects' => false], - 'array_pad' => ['hasSideEffects' => false], - 'array_product' => ['hasSideEffects' => false], - 'array_rand' => ['hasSideEffects' => false], - 'array_replace' => ['hasSideEffects' => false], - 'array_replace_recursive' => ['hasSideEffects' => false], - 'array_reverse' => ['hasSideEffects' => false], - 'array_slice' => ['hasSideEffects' => false], - 'array_sum' => ['hasSideEffects' => false], - 'array_udiff' => ['hasSideEffects' => false], - 'array_udiff_assoc' => ['hasSideEffects' => false], - 'array_udiff_uassoc' => ['hasSideEffects' => false], - 'array_uintersect' => ['hasSideEffects' => false], - 'array_uintersect_assoc' => ['hasSideEffects' => false], - 'array_uintersect_uassoc' => ['hasSideEffects' => false], - 'array_unique' => ['hasSideEffects' => false], - 'array_values' => ['hasSideEffects' => false], - 'asin' => ['hasSideEffects' => false], - 'asinh' => ['hasSideEffects' => false], - 'atan' => ['hasSideEffects' => false], - 'atan2' => ['hasSideEffects' => false], - 'atanh' => ['hasSideEffects' => false], - 'base64_decode' => ['hasSideEffects' => false], - 'base64_encode' => ['hasSideEffects' => false], - 'base_convert' => ['hasSideEffects' => false], - 'basename' => ['hasSideEffects' => false], - 'bcadd' => ['hasSideEffects' => false], - 'bccomp' => ['hasSideEffects' => false], - 'bcdiv' => ['hasSideEffects' => false], - 'bcmod' => ['hasSideEffects' => false], - 'bcmul' => ['hasSideEffects' => false], - // continue functionMap.php, line 424 - 'count' => ['hasSideEffects' => false], - 'sprintf' => ['hasSideEffects' => false], - - // methods - 'DateTime::createFromFormat' => ['hasSideEffects' => false], - 'DateTime::createFromImmutable' => ['hasSideEffects' => false], - 'DateTime::getLastErrors' => ['hasSideEffects' => false], - 'DateTime::add' => ['hasSideEffects' => true], - 'DateTime::modify' => ['hasSideEffects' => true], - 'DateTime::setDate' => ['hasSideEffects' => true], - 'DateTime::setISODate' => ['hasSideEffects' => true], - 'DateTime::setTime' => ['hasSideEffects' => true], - 'DateTime::setTimestamp' => ['hasSideEffects' => true], - 'DateTime::setTimezone' => ['hasSideEffects' => true], - 'DateTime::sub' => ['hasSideEffects' => true], - 'DateTime::diff' => ['hasSideEffects' => false], - 'DateTime::format' => ['hasSideEffects' => false], - 'DateTime::getOffset' => ['hasSideEffects' => false], - 'DateTime::getTimestamp' => ['hasSideEffects' => false], - 'DateTime::getTimezone' => ['hasSideEffects' => false], - - 'DateTimeImmutable::createFromFormat' => ['hasSideEffects' => false], - 'DateTimeImmutable::createFromMutable' => ['hasSideEffects' => false], - 'DateTimeImmutable::getLastErrors' => ['hasSideEffects' => false], - 'DateTimeImmutable::add' => ['hasSideEffects' => false], - 'DateTimeImmutable::modify' => ['hasSideEffects' => false], - 'DateTimeImmutable::setDate' => ['hasSideEffects' => false], - 'DateTimeImmutable::setISODate' => ['hasSideEffects' => false], - 'DateTimeImmutable::setTime' => ['hasSideEffects' => false], - 'DateTimeImmutable::setTimestamp' => ['hasSideEffects' => false], - 'DateTimeImmutable::setTimezone' => ['hasSideEffects' => false], - 'DateTimeImmutable::sub' => ['hasSideEffects' => false], - 'DateTimeImmutable::diff' => ['hasSideEffects' => false], - 'DateTimeImmutable::format' => ['hasSideEffects' => false], - 'DateTimeImmutable::getOffset' => ['hasSideEffects' => false], - 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], - 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], -]; diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 8da226e0fd..ea6b278145 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -2,13 +2,26 @@ 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; -class TrivialParametersAcceptor implements ParametersAcceptor +/** + * @api + */ +final class TrivialParametersAcceptor implements ExtendedParametersAcceptor, CallableParametersAcceptor { + /** @api */ + public function __construct(private string $callableName = 'callable') + { + } + public function getTemplateTypeMap(): TemplateTypeMap { return TemplateTypeMap::createEmpty(); @@ -19,9 +32,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - /** - * @return array - */ + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; @@ -37,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 new file mode 100644 index 0000000000..980a3f293f --- /dev/null +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -0,0 +1,144 @@ +transformStaticTypeCallback = $transformStaticTypeCallback; + } + + public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection + { + if ($this->cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self( + $this->methodReflection, + $this->resolvedDeclaringClass, + false, + $this->transformStaticTypeCallback, + ); + } + + public function getNakedMethod(): ExtendedMethodReflection + { + return $this->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, + ); + } + + public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection + { + return new CalledOnTypeUnresolvedMethodPrototypeReflection( + $this->methodReflection, + $this->resolvedDeclaringClass, + $this->resolveTemplateTypeMapToBounds, + $type, + ); + } + + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection + { + $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 + { + $callback = $this->transformStaticTypeCallback; + return $callback($type); + } + +} diff --git a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php new file mode 100644 index 0000000000..06069f8410 --- /dev/null +++ b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php @@ -0,0 +1,94 @@ +transformStaticTypeCallback = $transformStaticTypeCallback; + } + + public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection + { + if ($this->cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self( + $this->propertyReflection, + $this->resolvedDeclaringClass, + false, + $this->transformStaticTypeCallback, + ); + } + + public function getNakedProperty(): ExtendedPropertyReflection + { + return $this->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, + ); + } + + public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection + { + return new CalledOnTypeUnresolvedPropertyPrototypeReflection( + $this->propertyReflection, + $this->resolvedDeclaringClass, + $this->resolveTemplateTypeMapToBounds, + $type, + ); + } + + 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, $phpDocType, $nativeType); + } + + private function transformStaticType(Type $type): Type + { + $callback = $this->transformStaticTypeCallback; + return $callback($type); + } + +} diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php new file mode 100644 index 0000000000..c78a435583 --- /dev/null +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -0,0 +1,154 @@ +cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self( + $this->methodReflection, + $this->resolvedDeclaringClass, + false, + $this->calledOnType, + ); + } + + public function getNakedMethod(): ExtendedMethodReflection + { + return $this->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, + ); + } + + public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection + { + return new self( + $this->methodReflection, + $this->resolvedDeclaringClass, + $this->resolveTemplateTypeMapToBounds, + $type, + ); + } + + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection + { + $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; + } + + return $traverse($type); + }); + } + +} diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php new file mode 100644 index 0000000000..18beaf3f8e --- /dev/null +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -0,0 +1,94 @@ +cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self( + $this->propertyReflection, + $this->resolvedDeclaringClass, + false, + $this->fetchedOnType, + ); + } + + public function getNakedProperty(): ExtendedPropertyReflection + { + return $this->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, + ); + } + + public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection + { + return new self( + $this->propertyReflection, + $this->resolvedDeclaringClass, + $this->resolveTemplateTypeMapToBounds, + $type, + ); + } + + 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, $phpDocType, $nativeType); + } + + private function transformStaticType(Type $type): Type + { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof StaticType) { + return $this->fetchedOnType; + } + + return $traverse($type); + }); + } + +} diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index 9f0bbac6cf..eafd314157 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -2,31 +2,31 @@ 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 { - private string $methodName; - - /** @var MethodReflection[] */ - private array $methods; - /** - * @param string $methodName - * @param \PHPStan\Reflection\MethodReflection[] $methods + * @param ExtendedMethodReflection[] $methods */ - public function __construct(string $methodName, array $methods) + public function __construct(private string $methodName, private array $methods) { - $this->methodName = $methodName; - $this->methods = $methods; } public function getDeclaringClass(): ClassReflection @@ -79,36 +79,47 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - $variants = $this->methods[0]->getVariants(); - $returnType = TypeCombinator::intersect(...array_map(static function (MethodReflection $method): Type { - return TypeCombinator::intersect(...array_map(static function (ParametersAcceptor $acceptor): Type { - return $acceptor->getReturnType(); - }, $method->getVariants())); - }, $this->methods)); + $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 (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 array_map(static function (ParametersAcceptor $acceptor) use ($returnType): ParametersAcceptor { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - $acceptor->getParameters(), - $acceptor->isVariadic(), - $returnType - ); - }, $variants); + return $variants[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isDeprecated(); - }, $this->methods)); + return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated()); } public function getDeprecatedDescription(): ?string { $descriptions = []; foreach ($this->methods as $method) { - if ($method->isDeprecated()->yes()) { + if (!$method->isDeprecated()->yes()) { continue; } $description = $method->getDeprecatedDescription(); @@ -128,16 +139,22 @@ public function getDeprecatedDescription(): ?string public function isFinal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isFinal(); - }, $this->methods)); + return TrinaryLogic::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 function (MethodReflection $method): TrinaryLogic { - return $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 @@ -162,9 +179,12 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->hasSideEffects(); - }, $this->methods)); + return TrinaryLogic::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 @@ -172,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 new file mode 100644 index 0000000000..71de28e1e6 --- /dev/null +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -0,0 +1,203 @@ +properties[0]->getName(); + } + + public function getDeclaringClass(): ClassReflection + { + return $this->properties[0]->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); + } + + public function isPrivate(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); + } + + public function isPublic(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); + } + + public function getDeprecatedDescription(): ?string + { + $descriptions = []; + foreach ($this->properties as $property) { + if (!$property->isDeprecated()->yes()) { + continue; + } + $description = $property->getDeprecatedDescription(); + if ($description === null) { + continue; + } + + $descriptions[] = $description; + } + + if (count($descriptions) === 0) { + return null; + } + + return implode(' ', $descriptions); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); + } + + 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 (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); + } + + public function getWritableType(): Type + { + return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); + } + + public function canChangeTypeAfterAssignment(): bool + { + 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) { + $result = $result || $cb($property); + } + + return $result; + } + + 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->hasHook($hookType)) { + continue; + } + + $hooks[] = $property->getHook($hookType); + } + + 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 new file mode 100644 index 0000000000..fe3e09bd4e --- /dev/null +++ b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php @@ -0,0 +1,56 @@ +cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); + } + + public function getNakedMethod(): ExtendedMethodReflection + { + return $this->getTransformedMethod(); + } + + public function getTransformedMethod(): ExtendedMethodReflection + { + if ($this->transformedMethod !== null) { + return $this->transformedMethod; + } + $methods = array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): MethodReflection => $prototype->getTransformedMethod(), $this->methodPrototypes); + + return $this->transformedMethod = new IntersectionTypeMethodReflection($this->methodName, $methods); + } + + public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection + { + return new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->withCalledOnType($type), $this->methodPrototypes)); + } + +} diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php new file mode 100644 index 0000000000..51e6ccaf46 --- /dev/null +++ b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php @@ -0,0 +1,56 @@ +cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); + } + + public function getNakedProperty(): ExtendedPropertyReflection + { + return $this->getTransformedProperty(); + } + + public function getTransformedProperty(): ExtendedPropertyReflection + { + if ($this->transformedProperty !== null) { + return $this->transformedProperty; + } + $properties = array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection => $prototype->getTransformedProperty(), $this->propertyPrototypes); + + return $this->transformedProperty = new IntersectionTypePropertyReflection($properties); + } + + public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection + { + return new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->withFechedOnType($type), $this->propertyPrototypes)); + } + +} diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index f133933826..b330b6fdad 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -2,31 +2,30 @@ 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 { - private string $methodName; - - /** @var MethodReflection[] */ - private array $methods; - /** - * @param string $methodName - * @param \PHPStan\Reflection\MethodReflection[] $methods + * @param ExtendedMethodReflection[] $methods */ - public function __construct(string $methodName, array $methods) + public function __construct(private string $methodName, private array $methods) { - $this->methodName = $methodName; - $this->methods = $methods; } public function getDeclaringClass(): ClassReflection @@ -79,36 +78,31 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - $variants = $this->methods[0]->getVariants(); - $returnType = TypeCombinator::union(...array_map(static function (MethodReflection $method): Type { - return TypeCombinator::union(...array_map(static function (ParametersAcceptor $acceptor): Type { - return $acceptor->getReturnType(); - }, $method->getVariants())); - }, $this->methods)); + $variants = array_merge(...array_map(static fn (MethodReflection $method) => $method->getVariants(), $this->methods)); + + return [ParametersAcceptorSelector::combineAcceptors($variants)]; + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } - return array_map(static function (ParametersAcceptor $acceptor) use ($returnType): ParametersAcceptor { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - $acceptor->getParameters(), - $acceptor->isVariadic(), - $returnType - ); - }, $variants); + public function getNamedArgumentsVariants(): ?array + { + return null; } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isDeprecated(); - }, $this->methods)); + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated()); } public function getDeprecatedDescription(): ?string { $descriptions = []; foreach ($this->methods as $method) { - if ($method->isDeprecated()->yes()) { + if (!$method->isDeprecated()->yes()) { continue; } $description = $method->getDeprecatedDescription(); @@ -128,16 +122,22 @@ public function getDeprecatedDescription(): ?string public function isFinal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isFinal(); - }, $this->methods)); + return TrinaryLogic::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 function (MethodReflection $method): TrinaryLogic { - return $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 @@ -162,9 +162,12 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->hasSideEffects(); - }, $this->methods)); + return TrinaryLogic::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 @@ -172,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 new file mode 100644 index 0000000000..77e1ed0397 --- /dev/null +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -0,0 +1,203 @@ +properties[0]->getName(); + } + + public function getDeclaringClass(): ClassReflection + { + return $this->properties[0]->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isStatic()); + } + + public function isPrivate(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPrivate()); + } + + public function isPublic(): bool + { + return $this->computeResult(static fn (ExtendedPropertyReflection $property) => $property->isPublic()); + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isDeprecated()); + } + + public function getDeprecatedDescription(): ?string + { + $descriptions = []; + foreach ($this->properties as $property) { + if (!$property->isDeprecated()->yes()) { + continue; + } + $description = $property->getDeprecatedDescription(); + if ($description === null) { + continue; + } + + $descriptions[] = $description; + } + + if (count($descriptions) === 0) { + return null; + } + + return implode(' ', $descriptions); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isInternal()); + } + + 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 (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties)); + } + + public function getWritableType(): Type + { + return TypeCombinator::union(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties)); + } + + public function canChangeTypeAfterAssignment(): bool + { + 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) { + $result = $result && $cb($property); + } + + return $result; + } + + 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->hasHook($hookType)) { + continue; + } + + $hooks[] = $property->getHook($hookType); + } + + 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 new file mode 100644 index 0000000000..627a1e5b80 --- /dev/null +++ b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php @@ -0,0 +1,57 @@ +cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); + } + + public function getNakedMethod(): ExtendedMethodReflection + { + return $this->getTransformedMethod(); + } + + public function getTransformedMethod(): ExtendedMethodReflection + { + if ($this->transformedMethod !== null) { + return $this->transformedMethod; + } + + $methods = array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): MethodReflection => $prototype->getTransformedMethod(), $this->methodPrototypes); + + return $this->transformedMethod = new UnionTypeMethodReflection($this->methodName, $methods); + } + + public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection + { + return new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->withCalledOnType($type), $this->methodPrototypes)); + } + +} diff --git a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php new file mode 100644 index 0000000000..b28625e3c3 --- /dev/null +++ b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php @@ -0,0 +1,56 @@ +cachedDoNotResolveTemplateTypeMapToBounds !== null) { + return $this->cachedDoNotResolveTemplateTypeMapToBounds; + } + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); + } + + public function getNakedProperty(): ExtendedPropertyReflection + { + return $this->getTransformedProperty(); + } + + public function getTransformedProperty(): ExtendedPropertyReflection + { + if ($this->transformedProperty !== null) { + return $this->transformedProperty; + } + + $methods = array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection => $prototype->getTransformedProperty(), $this->propertyPrototypes); + + return $this->transformedProperty = new UnionTypePropertyReflection($methods); + } + + public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection + { + return new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->withFechedOnType($type), $this->propertyPrototypes)); + } + +} diff --git a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php new file mode 100644 index 0000000000..4665670cb4 --- /dev/null +++ b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000000..e7bb397ace --- /dev/null +++ b/src/Reflection/WrapperPropertyReflection.php @@ -0,0 +1,10 @@ + + */ +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 new file mode 100644 index 0000000000..cf908f004b --- /dev/null +++ b/src/Rules/Api/ApiClassExtendsRule.php @@ -0,0 +1,76 @@ + + */ +final class ApiClassExtendsRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->extends === null) { + return []; + } + + $extendedClassName = (string) $node->extends; + if (!$this->reflectionProvider->hasClass($extendedClassName)) { + return []; + } + + $extendedClassReflection = $this->reflectionProvider->getClass($extendedClassName); + if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedClassReflection->getName(), $extendedClassReflection->getFileName())) { + return []; + } + + if ($extendedClassReflection->getName() === MutatingScope::class) { + return []; + } + + $ruleError = RuleErrorBuilder::message(sprintf( + 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + $extendedClassReflection->getDisplayName(), + ))->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(); + + $docBlock = $extendedClassReflection->getResolvedPhpDoc(); + if ($docBlock === null) { + return [$ruleError]; + } + + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return []; + } + } + + return [$ruleError]; + } + +} diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php new file mode 100644 index 0000000000..d1615303b5 --- /dev/null +++ b/src/Rules/Api/ApiClassImplementsRule.php @@ -0,0 +1,87 @@ + + */ +final class ApiClassImplementsRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + foreach ($node->implements as $implements) { + $errors = array_merge($errors, $this->checkName($scope, $implements)); + } + + return $errors; + } + + /** + * @return list + */ + private function checkName(Scope $scope, Node\Name $name): array + { + $implementedClassName = (string) $name; + if (!$this->reflectionProvider->hasClass($implementedClassName)) { + return []; + } + + $implementedClassReflection = $this->reflectionProvider->getClass($implementedClassName); + 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(), + ))->identifier('phpstanApi.interface')->tip(sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(); + + if (in_array($implementedClassReflection->getName(), BcUncoveredInterface::CLASSES, true)) { + return [$ruleError]; + } + + $docBlock = $implementedClassReflection->getResolvedPhpDoc(); + if ($docBlock === null) { + return [$ruleError]; + } + + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return []; + } + } + + 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 new file mode 100644 index 0000000000..80079c5495 --- /dev/null +++ b/src/Rules/Api/ApiInstantiationRule.php @@ -0,0 +1,76 @@ + + */ +final class ApiInstantiationRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + 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 (!$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(), + ))->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(); + + if (!$classReflection->hasConstructor()) { + return [$ruleError]; + } + + $constructor = $classReflection->getConstructor(); + $docComment = $constructor->getDocComment(); + if ($docComment === null) { + return [$ruleError]; + } + + if (!str_contains($docComment, '@api')) { + return [$ruleError]; + } + + if ($constructor->getDeclaringClass()->getName() !== $classReflection->getName()) { + return [$ruleError]; + } + + return []; + } + +} diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php new file mode 100644 index 0000000000..048aa82a1b --- /dev/null +++ b/src/Rules/Api/ApiInterfaceExtendsRule.php @@ -0,0 +1,87 @@ + + */ +final class ApiInterfaceExtendsRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Interface_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + foreach ($node->extends as $extends) { + $errors = array_merge($errors, $this->checkName($scope, $extends)); + } + + return $errors; + } + + /** + * @return list + */ + private function checkName(Scope $scope, Node\Name $name): array + { + $extendedInterface = (string) $name; + if (!$this->reflectionProvider->hasClass($extendedInterface)) { + return []; + } + + $extendedInterfaceReflection = $this->reflectionProvider->getClass($extendedInterface); + 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(), + ))->identifier('phpstanApi.interface')->tip(sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(); + + if (in_array($extendedInterfaceReflection->getName(), BcUncoveredInterface::CLASSES, true)) { + return [$ruleError]; + } + + $docBlock = $extendedInterfaceReflection->getResolvedPhpDoc(); + if ($docBlock === null) { + return [$ruleError]; + } + + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return []; + } + } + + return [$ruleError]; + } + +} diff --git a/src/Rules/Api/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php new file mode 100644 index 0000000000..8c733903c4 --- /dev/null +++ b/src/Rules/Api/ApiMethodCallRule.php @@ -0,0 +1,82 @@ + + */ +final class ApiMethodCallRule implements Rule +{ + + public function __construct(private ApiRuleHelper $apiRuleHelper) + { + } + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + + $methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString()); + if ($methodReflection === null) { + return []; + } + + $declaringClass = $methodReflection->getDeclaringClass(); + if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName())) { + return []; + } + + if ($this->isCovered($methodReflection)) { + return []; + } + + $ruleError = RuleErrorBuilder::message(sprintf( + 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', + $declaringClass->getDisplayName(), + $methodReflection->getName(), + ))->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(); + + return [$ruleError]; + } + + private function isCovered(MethodReflection $methodReflection): bool + { + $declaringClass = $methodReflection->getDeclaringClass(); + $classDocBlock = $declaringClass->getResolvedPhpDoc(); + if ($classDocBlock !== null) { + foreach ($classDocBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return true; + } + } + } + + $methodDocComment = $methodReflection->getDocComment(); + if ($methodDocComment === null) { + return false; + } + + return str_contains($methodDocComment, '@api'); + } + +} diff --git a/src/Rules/Api/ApiRuleHelper.php b/src/Rules/Api/ApiRuleHelper.php new file mode 100644 index 0000000000..8fe066e60b --- /dev/null +++ b/src/Rules/Api/ApiRuleHelper.php @@ -0,0 +1,86 @@ +getNamespace(); + if ($scopeNamespace === null) { + return $this->isPhpStanName($namespace); + } + + if ($this->isPhpStanName($scopeNamespace)) { + if (!$this->isPhpStanName($namespace)) { + return false; + } + + if ($declaringFile !== null) { + $scopeFile = $scope->getFile(); + $dir = dirname($scopeFile); + $helper = new ParentDirectoryRelativePathHelper($dir); + $pathParts = $helper->getFilenameParts($declaringFile); + $directories = $this->createAbsoluteDirectories($dir, $pathParts); + foreach ($directories as $directory) { + if (pathinfo($directory, PATHINFO_BASENAME) === 'vendor') { + return true; + } + } + } + + return false; + } + + return $this->isPhpStanName($namespace); + } + + /** + * @param string[] $parts + * @return string[] + */ + private function createAbsoluteDirectories(string $currentDirectory, array $parts): array + { + $directories = []; + foreach ($parts as $part) { + if ($part === '..') { + $currentDirectory = dirname($currentDirectory); + $directories[] = $currentDirectory; + continue; + } + + $currentDirectory .= '/' . $part; + $directories[] = $currentDirectory; + } + + return $directories; + } + + public function isPhpStanName(string $namespace): bool + { + if (strtolower($namespace) === 'phpstan') { + return true; + } + + if (str_starts_with($namespace, 'PHPStan\\PhpDocParser\\')) { + return false; + } + + if (str_starts_with($namespace, 'PHPStan\\BetterReflection\\')) { + return false; + } + + return stripos($namespace, 'PHPStan\\') === 0; + } + +} diff --git a/src/Rules/Api/ApiStaticCallRule.php b/src/Rules/Api/ApiStaticCallRule.php new file mode 100644 index 0000000000..8c1b53457a --- /dev/null +++ b/src/Rules/Api/ApiStaticCallRule.php @@ -0,0 +1,97 @@ + + */ +final class ApiStaticCallRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\StaticCall::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); + $methodName = $node->name->toString(); + if (!$classReflection->hasNativeMethod($methodName)) { + return []; + } + + $methodReflection = $classReflection->getNativeMethod($methodName); + $declaringClass = $methodReflection->getDeclaringClass(); + if (!$this->apiRuleHelper->isPhpStanCode($scope, $declaringClass->getName(), $declaringClass->getFileName())) { + return []; + } + + if ($this->isCovered($methodReflection)) { + return []; + } + + $ruleError = RuleErrorBuilder::message(sprintf( + 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', + $declaringClass->getDisplayName(), + $methodReflection->getName(), + ))->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(); + + return [$ruleError]; + } + + private function isCovered(MethodReflection $methodReflection): bool + { + $declaringClass = $methodReflection->getDeclaringClass(); + $classDocBlock = $declaringClass->getResolvedPhpDoc(); + if ($methodReflection->getName() !== '__construct' && $classDocBlock !== null) { + foreach ($classDocBlock->getPhpDocNodes() as $phpDocNode) { + $apiTags = $phpDocNode->getTagsByName('@api'); + if (count($apiTags) > 0) { + return true; + } + } + } + + $methodDocComment = $methodReflection->getDocComment(); + if ($methodDocComment === null) { + return false; + } + + return str_contains($methodDocComment, '@api'); + } + +} diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php new file mode 100644 index 0000000000..a0074cbd4a --- /dev/null +++ b/src/Rules/Api/ApiTraitUseRule.php @@ -0,0 +1,57 @@ + + */ +final class ApiTraitUseRule implements Rule +{ + + public function __construct( + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\TraitUse::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $tip = sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ); + foreach ($node->traits as $traitName) { + $traitName = $traitName->toString(); + if (!$this->reflectionProvider->hasClass($traitName)) { + continue; + } + + $traitReflection = $this->reflectionProvider->getClass($traitName); + 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(), + ))->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..282c2189d2 --- /dev/null +++ b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php @@ -0,0 +1,79 @@ + + */ +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 []; + } + + $messages = []; + $argType = $scope->getType($args[0]->value); + foreach ($argType->getConstantStrings() as $constantString) { + $argValue = $constantString->getValue(); + if (!in_array($argValue, ['parent', 'previous', 'next'], true)) { + continue; + } + + if (!$scope->isInClass()) { + continue; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf('Node attribute \'%s\' is no longer available.', $argValue)) + ->identifier('phpParser.nodeConnectingAttribute') + ->tip('See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules') + ->build(); + } + + return $messages; + } + +} 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 new file mode 100644 index 0000000000..8547f7b2ed --- /dev/null +++ b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php @@ -0,0 +1,89 @@ + + */ +final class PhpStanNamespaceIn3rdPartyPackageRule implements Rule +{ + + public function __construct(private ApiRuleHelper $apiRuleHelper) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Namespace_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $namespace = null; + if ($node->name !== null) { + $namespace = $node->name->toString(); + } + if ($namespace === null || !$this->apiRuleHelper->isPhpStanName($namespace)) { + return []; + } + + $composerJson = $this->findComposerJsonContents(dirname($scope->getFile())); + if ($composerJson === null) { + return []; + } + + $packageName = $composerJson['name'] ?? null; + 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(), + ]; + } + + /** + * @return mixed[]|null + */ + private function findComposerJsonContents(string $fromDirectory): ?array + { + if (!is_dir($fromDirectory)) { + return null; + } + + $composerJsonPath = $fromDirectory . '/composer.json'; + if (!is_file($composerJsonPath)) { + $dirName = dirname($fromDirectory); + if ($dirName !== $fromDirectory) { + return $this->findComposerJsonContents($dirName); + } + + return null; + } + + try { + return Json::decode(FileReader::read($composerJsonPath), Json::FORCE_ARRAY); + } catch (JsonException) { + return null; + } catch (CouldNotReadFileException) { + return null; + } + } + +} 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 7e16863f0d..0000000000 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ /dev/null @@ -1,90 +0,0 @@ - - */ -class AppendedArrayItemTypeRule implements \PHPStan\Rules\Rule -{ - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct( - PropertyReflectionFinder $propertyReflectionFinder, - RuleLevelHelper $ruleLevelHelper - ) - { - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->ruleLevelHelper = $ruleLevelHelper; - } - - public function getNodeType(): string - { - return \PhpParser\Node\Expr::class; - } - - public function processNode(\PhpParser\Node $node, Scope $scope): array - { - if ( - !$node instanceof Assign - && !$node instanceof AssignOp - ) { - return []; - } - - if (!($node->var instanceof ArrayDimFetch)) { - return []; - } - - if ( - !$node->var->var instanceof \PhpParser\Node\Expr\PropertyFetch - && !$node->var->var instanceof \PhpParser\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) { - $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); - 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 1dd50f00c1..0000000000 --- a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php +++ /dev/null @@ -1,91 +0,0 @@ - - */ -class AppendedArrayKeyTypeRule implements \PHPStan\Rules\Rule -{ - - private PropertyReflectionFinder $propertyReflectionFinder; - - private bool $checkUnionTypes; - - public function __construct( - PropertyReflectionFinder $propertyReflectionFinder, - bool $checkUnionTypes - ) - { - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->checkUnionTypes = $checkUnionTypes; - } - - public function getNodeType(): string - { - return Assign::class; - } - - public function processNode(\PhpParser\Node $node, Scope $scope): array - { - if (!($node->var instanceof ArrayDimFetch)) { - return []; - } - - if ( - !$node->var->var instanceof \PhpParser\Node\Expr\PropertyFetch - && !$node->var->var instanceof \PhpParser\Node\Expr\StaticPropertyFetch - ) { - 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()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Array (%s) does not accept key %s.', - $arrayType->describe(VerbosityLevel::typeOnly()), - $keyType->describe(VerbosityLevel::value()) - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php new file mode 100644 index 0000000000..1d9537d274 --- /dev/null +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -0,0 +1,117 @@ + + */ +final class ArrayDestructuringRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, + ) + { + } + + public function getNodeType(): string + { + return Assign::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->var instanceof Node\Expr\List_) { + return []; + } + + return $this->getErrors( + $scope, + $node->var, + $node->expr, + ); + } + + /** + * @return list + */ + private function getErrors(Scope $scope, Node\Expr\List_ $var, Expr $expr): array + { + $exprTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $expr, + '', + static fn (Type $varType): bool => $varType->isArray()->yes() || (new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes(), + ); + $exprType = $exprTypeResult->getType(); + if ($exprType instanceof ErrorType) { + return []; + } + if (!$exprType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($exprType)->yes()) { + return [ + RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly()))) + ->identifier('offsetAccess.nonArray') + ->build(), + ]; + } + + $errors = []; + $i = 0; + foreach ($var->items as $item) { + if ($item === null) { + $i++; + continue; + } + + $keyExpr = null; + if ($item->key === null) { + $keyType = new ConstantIntegerType($i); + $keyExpr = new Node\Scalar\Int_($i); + } else { + $keyType = $scope->getType($item->key); + $keyExpr = new TypeExpr($keyType); + } + + $itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check( + $scope, + $expr, + '', + $keyType, + ); + $errors = array_merge($errors, $itemErrors); + + if (!$item->value instanceof Node\Expr\List_) { + $i++; + continue; + } + + $errors = array_merge($errors, $this->getErrors( + $scope, + $item->value, + new Expr\ArrayDimFetch($expr, $keyExpr), + )); + } + + return $errors; + } + +} 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 f24d930be3..61d2085fec 100644 --- a/src/Rules/Arrays/DeadForeachRule.php +++ b/src/Rules/Arrays/DeadForeachRule.php @@ -8,9 +8,9 @@ use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Foreach_> + * @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 e972c5fbde..7e54a2cb15 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -2,24 +2,31 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\LiteralArrayNode; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\Constant\ConstantIntegerType; +use function array_keys; +use function count; +use function implode; +use function is_int; +use function max; +use function sprintf; +use function var_export; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\LiteralArrayNode> + * @implements Rule */ -class DuplicateKeysInLiteralArraysRule implements \PHPStan\Rules\Rule +final class DuplicateKeysInLiteralArraysRule implements Rule { - private \PhpParser\PrettyPrinter\Standard $printer; - public function __construct( - \PhpParser\PrettyPrinter\Standard $printer + private ExprPrinter $exprPrinter, ) { - $this->printer = $printer; } public function getNodeType(): string @@ -27,41 +34,74 @@ public function getNodeType(): string return LiteralArrayNode::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { $values = []; $duplicateKeys = []; $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->key === null) { + if ($item === null) { + $autoGeneratedIndex = false; continue; } $key = $item->key; - $keyType = $itemNode->getScope()->getType($key); - if ( - !$keyType instanceof ConstantScalarType - ) { - continue; - } - - $printedValue = $this->printer->prettyPrintExpr($key); - $value = $keyType->getValue(); - $printedValues[$value][] = $printedValue; + if ($key === null) { + if ($autoGeneratedIndex === false) { + continue; + } - if (!isset($valueLines[$value])) { - $valueLines[$value] = $item->getLine(); + if ($autoGeneratedIndex === null) { + $autoGeneratedIndex = 0; + $keyType = new ConstantIntegerType(0); + } else { + $keyType = new ConstantIntegerType(++$autoGeneratedIndex); + } + } else { + $keyType = $itemNode->getScope()->getType($key); + $arrayKeyValues = $keyType->toArrayKey()->getConstantScalarValues(); + if (count($arrayKeyValues) === 1 && is_int($arrayKeyValues[0])) { + $autoGeneratedIndex = $autoGeneratedIndex === null + ? $arrayKeyValues[0] + : max($autoGeneratedIndex, $arrayKeyValues[0]); + } } - $previousCount = count($values); - $values[$value] = $printedValue; - if ($previousCount !== count($values)) { + $keyValues = $keyType->getConstantScalarValues(); + if (count($keyValues) === 0) { + $autoGeneratedIndex = false; continue; } - $duplicateKeys[$value] = true; + foreach ($keyValues as $value) { + $printedValue = $key !== null + ? $this->exprPrinter->printExpr($key) + : $value; + $printedValues[$value][] = $printedValue; + + if (!isset($valueLines[$value])) { + $valueLines[$value] = $item->getStartLine(); + } + + $previousCount = count($values); + $values[$value] = $printedValue; + if ($previousCount !== count($values)) { + continue; + } + + $duplicateKeys[$value] = true; + } } $messages = []; @@ -71,8 +111,8 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array count($printedValues[$value]), count($printedValues[$value]) === 1 ? 'duplicate key' : 'duplicate keys', var_export($value, true), - implode(', ', $printedValues[$value]) - ))->line($valueLines[$value])->build(); + implode(', ', $printedValues[$value]), + ))->identifier('array.duplicateKey')->line($valueLines[$value])->build(); } return $messages; diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 18b771b859..e74b192a67 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -2,58 +2,67 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class InvalidKeyInArrayDimFetchRule implements \PHPStan\Rules\Rule +final class InvalidKeyInArrayDimFetchRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, + ) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->dim === null) { return []; } - $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 1356bc501c..fb4ab23162 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -2,30 +2,30 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayItem> + * @implements Rule */ -class InvalidKeyInArrayItemRule implements \PHPStan\Rules\Rule +final class InvalidKeyInArrayItemRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct(private bool $reportMaybes) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayItem::class; + return Node\ArrayItem::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->key === null) { return []; @@ -36,14 +36,14 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array if ($isSuperType->no()) { return [ RuleErrorBuilder::message( - sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())) - )->build(), + sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), + )->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(), + sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), + )->identifier('array.invalidKey')->build(), ]; } diff --git a/src/Rules/Arrays/IterableInForeachRule.php b/src/Rules/Arrays/IterableInForeachRule.php index dafc1c56d7..fd6351a003 100644 --- a/src/Rules/Arrays/IterableInForeachRule.php +++ b/src/Rules/Arrays/IterableInForeachRule.php @@ -4,39 +4,38 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InForeachNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Foreach_> + * @implements Rule */ -class IterableInForeachRule implements \PHPStan\Rules\Rule +final class IterableInForeachRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\Foreach_::class; + return InForeachNode::class; } public function processNode(Node $node, Scope $scope): array { + $originalNode = $node->getOriginalNode(); $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->expr, + $originalNode->expr, 'Iterating over an object of an unknown class %s.', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -49,8 +48,8 @@ static function (Type $type): bool { return [ RuleErrorBuilder::message(sprintf( 'Argument of an invalid type %s supplied for foreach, only iterables are supported.', - $type->describe(VerbosityLevel::typeOnly()) - ))->line($node->expr->getLine())->build(), + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('foreach.nonIterable')->line($originalNode->expr->getStartLine())->build(), ]; } diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php new file mode 100644 index 0000000000..5b81f82f1a --- /dev/null +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -0,0 +1,118 @@ + + */ + public function check( + Scope $scope, + Expr $var, + string $unknownClassPattern, + Type $dimType, + ): array + { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $var), + $unknownClassPattern, + static fn (Type $type): bool => $type->hasOffsetValueType($dimType)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + 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 ($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; + break; + } + continue; + } + foreach (TypeUtils::flattenTypes($dimType) as $innerDimType) { + if ($innerType->hasOffsetValueType($innerDimType)->no()) { + $report = true; + break 2; + } + } + } + + 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 []; + } + +} diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 8f562e92ca..b0edfcadf9 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -2,44 +2,45 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function in_array; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class NonexistentOffsetInArrayDimFetchRule implements \PHPStan\Rules\Rule +final class NonexistentOffsetInArrayDimFetchRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - bool $reportMaybes + private RuleLevelHelper $ruleLevelHelper, + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, + private bool $reportMaybes, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->dim !== null) { $dimType = $scope->getType($node->dim); - $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', $dimType->describe(VerbosityLevel::value())); + $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', SprintfHelper::escapeFormatString($dimType->describe(VerbosityLevel::value()))); } else { $dimType = null; $unknownClassPattern = 'Access to an offset on an unknown class %s.'; @@ -47,40 +48,46 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array $isOffsetAccessibleTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), $unknownClassPattern, - static function (Type $type): bool { - return $type->isOffsetAccessible()->yes(); - } + static fn (Type $type): bool => $type->isOffsetAccessible()->yes(), ); $isOffsetAccessibleType = $isOffsetAccessibleTypeResult->getType(); if ($isOffsetAccessibleType instanceof ErrorType) { 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()), - $isOffsetAccessibleType->describe(VerbosityLevel::value()) - ))->build(), + $dimType->describe(count($dimType->getConstantStrings()) > 0 ? VerbosityLevel::precise() : VerbosityLevel::value()), + $isOffsetAccessibleType->describe(VerbosityLevel::value()), + ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; } return [ RuleErrorBuilder::message(sprintf( 'Cannot access an offset on %s.', - $isOffsetAccessibleType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $isOffsetAccessibleType->describe(VerbosityLevel::typeOnly()), + ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; } @@ -91,58 +98,55 @@ static function (Type $type): bool { return []; } - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->var, - $unknownClassPattern, - static function (Type $type) use ($dimType): bool { - return $type->hasOffsetValueType($dimType)->yes(); - } - ); - $type = $typeResult->getType(); - if ($type instanceof ErrorType) { - 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 ( + $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 (!$report && $this->reportMaybes) { - foreach (TypeUtils::flattenTypes($type) as $innerType) { - if ($dimType instanceof UnionType) { - if ($innerType->hasOffsetValueType($dimType)->no()) { - $report = true; - break; - } - continue; - } - foreach (TypeUtils::flattenTypes($dimType) as $innerDimType) { - if ($innerType->hasOffsetValueType($innerDimType)->no()) { - $report = true; - break; - } - } + 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 []; } } - if ($report) { - return [ - RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(), - ]; - } - - return []; + return $this->nonexistentOffsetInArrayDimFetchCheck->check( + $scope, + $node->var, + $unknownClassPattern, + $dimType, + ); } } diff --git a/src/Rules/Arrays/OffsetAccessAssignOpRule.php b/src/Rules/Arrays/OffsetAccessAssignOpRule.php index 054d988fbd..5ab745647b 100644 --- a/src/Rules/Arrays/OffsetAccessAssignOpRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignOpRule.php @@ -2,34 +2,34 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\AssignOp> + * @implements Rule */ -class OffsetAccessAssignOpRule implements \PHPStan\Rules\Rule +final class OffsetAccessAssignOpRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr\AssignOp::class; + return Node\Expr\AssignOp::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if (!$node->var instanceof ArrayDimFetch) { return []; @@ -49,7 +49,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array static function (Type $varType) use ($potentialDimType): bool { $arrayDimType = $varType->setOffsetValueType($potentialDimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $varType = $varTypeResult->getType(); @@ -61,7 +61,7 @@ static function (Type $varType) use ($potentialDimType): bool { static function (Type $dimType) use ($varType): bool { $arrayDimType = $varType->setOffsetValueType($dimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $dimType = $dimTypeResult->getType(); if ($varType->hasOffsetValueType($dimType)->no()) { @@ -80,8 +80,8 @@ static function (Type $dimType) use ($varType): bool { return [ RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', - $varType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $varType->describe(VerbosityLevel::typeOnly()), + ))->identifier('offsetAssign.dimType')->build(), ]; } @@ -89,8 +89,8 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), - $varType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $varType->describe(VerbosityLevel::typeOnly()), + ))->identifier('offsetAssign.dimType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessAssignmentRule.php b/src/Rules/Arrays/OffsetAccessAssignmentRule.php index e111fca026..c6c0f92b14 100644 --- a/src/Rules/Arrays/OffsetAccessAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignmentRule.php @@ -2,33 +2,34 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class OffsetAccessAssignmentRule implements \PHPStan\Rules\Rule +final class OffsetAccessAssignmentRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if (!$scope->isInExpressionAssign($node)) { return []; @@ -41,12 +42,12 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array $varTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), '', static function (Type $varType) use ($potentialDimType): bool { $arrayDimType = $varType->setOffsetValueType($potentialDimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $varType = $varTypeResult->getType(); if ($varType instanceof ErrorType) { @@ -64,7 +65,7 @@ static function (Type $varType) use ($potentialDimType): bool { static function (Type $dimType) use ($varType): bool { $arrayDimType = $varType->setOffsetValueType($dimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $dimType = $dimTypeResult->getType(); } else { @@ -80,8 +81,8 @@ static function (Type $dimType) use ($varType): bool { return [ RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', - $varType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $varType->describe(VerbosityLevel::typeOnly()), + ))->identifier('offsetAssign.dimType')->build(), ]; } @@ -89,8 +90,8 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), - $varType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $varType->describe(VerbosityLevel::typeOnly()), + ))->identifier('offsetAssign.dimType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 12e62f625a..9145005d20 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -2,28 +2,28 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\AssignOp; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class OffsetAccessValueAssignmentRule implements \PHPStan\Rules\Rule +final class OffsetAccessValueAssignmentRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -31,11 +31,12 @@ public function getNodeType(): string return Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( !$node instanceof Assign && !$node instanceof AssignOp + && !$node instanceof Expr\AssignRef ) { return []; } @@ -45,14 +46,17 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } $arrayDimFetch = $node->var; + $varType = $scope->getType($arrayDimFetch->var); + if ($varType->isObject()->no()) { + return []; + } - if ($node instanceof Assign) { + if ($node instanceof Assign || $node instanceof Expr\AssignRef) { $assignedValueType = $scope->getType($node->expr); } else { $assignedValueType = $scope->getType($node); } - $originalArrayType = $scope->getType($arrayDimFetch->var); $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, @@ -60,7 +64,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array static function (Type $varType) use ($assignedValueType): bool { $result = $varType->setOffsetValueType(new MixedType(), $assignedValueType); return !($result instanceof ErrorType); - } + }, ); $arrayType = $arrayTypeResult->getType(); if ($arrayType instanceof ErrorType) { @@ -75,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(), + $assignedValueType->describe(VerbosityLevel::typeOnly()), + ))->identifier('offsetAssign.valueType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php index 5a19af0357..8d83452357 100644 --- a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php +++ b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php @@ -2,21 +2,23 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class OffsetAccessWithoutDimForReadingRule implements \PHPStan\Rules\Rule +final class OffsetAccessWithoutDimForReadingRule implements Rule { public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($scope->isInExpressionAssign($node)) { return []; @@ -27,7 +29,10 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Cannot use [] for reading.')->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 9362c85cbb..bc515b9d04 100644 --- a/src/Rules/Arrays/UnpackIterableInArrayRule.php +++ b/src/Rules/Arrays/UnpackIterableInArrayRule.php @@ -11,20 +11,18 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\LiteralArrayNode> + * @implements Rule */ -class UnpackIterableInArrayRule implements Rule +final class UnpackIterableInArrayRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -37,6 +35,9 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($node->getItemNodes() as $itemNode) { $item = $itemNode->getArrayItem(); + if ($item === null) { + continue; + } if (!$item->unpack) { continue; } @@ -45,9 +46,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $item->value, '', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -60,8 +59,8 @@ static function (Type $type): bool { $errors[] = RuleErrorBuilder::message(sprintf( 'Only iterables can be unpacked, %s given.', - $type->describe(VerbosityLevel::typeOnly()) - ))->line($item->getLine())->build(); + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('arrayUnpacking.nonIterable')->line($item->getStartLine())->build(); } return $errors; diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php new file mode 100644 index 0000000000..c391d69123 --- /dev/null +++ b/src/Rules/AttributesCheck.php @@ -0,0 +1,167 @@ + $requiredTarget + * @return list + */ + public function check( + Scope $scope, + array $attrGroups, + int $requiredTarget, + string $targetName, + ): array + { + $errors = []; + $alreadyPresent = []; + foreach ($attrGroups as $attrGroup) { + 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->getStartLine()) + ->identifier('attribute.notFound') + ->build(); + continue; + } + + $attributeClass = $this->reflectionProvider->getClass($name); + if (!$attributeClass->isAttributeClass()) { + $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)) + ->identifier('attribute.abstract') + ->line($attribute->getStartLine()) + ->build(); + } + + 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)) + ->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)) + ->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)) + ->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)) + ->identifier('attribute.constructorNotPublic') + ->line($attribute->getStartLine()) + ->build(); + } + + $attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName()); + + $nodeAttributes = $attribute->getAttributes(); + $nodeAttributes['isAttribute'] = true; + + $parameterErrors = $this->functionCallParametersCheck->check( + ParametersAcceptorSelector::selectFromArgs( + $scope, + $attribute->args, + $attributeConstructor->getVariants(), + $attributeConstructor->getNamedArgumentsVariants(), + ), + $scope, + $attributeConstructor->getDeclaringClass()->isBuiltin(), + new New_($attribute->name, $attribute->args, $nodeAttributes), + '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) { + $errors[] = $error; + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Cast/EchoRule.php b/src/Rules/Cast/EchoRule.php index b0ede225b3..697822b55f 100644 --- a/src/Rules/Cast/EchoRule.php +++ b/src/Rules/Cast/EchoRule.php @@ -10,18 +10,16 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Echo_> + * @implements Rule */ -class EchoRule implements Rule +final class EchoRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -38,9 +36,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $expr, '', - static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - } + static fn (Type $type): bool => !$type->toString() instanceof ErrorType, ); if ($typeResult->getType() instanceof ErrorType @@ -52,8 +48,8 @@ static function (Type $type): bool { $messages[] = RuleErrorBuilder::message(sprintf( 'Parameter #%d (%s) of echo cannot be converted to string.', $key + 1, - $typeResult->getType()->describe(VerbosityLevel::value()) - ))->line($expr->getLine())->build(); + $typeResult->getType()->describe(VerbosityLevel::value()), + ))->identifier('echo.nonString')->line($expr->getStartLine())->build(); } return $messages; } diff --git a/src/Rules/Cast/InvalidCastRule.php b/src/Rules/Cast/InvalidCastRule.php index 7a2e63be5f..4e3bddad6e 100644 --- a/src/Rules/Cast/InvalidCastRule.php +++ b/src/Rules/Cast/InvalidCastRule.php @@ -5,47 +5,46 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function get_class; +use function sprintf; +use function strtolower; +use function substr; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Cast> + * @implements Rule */ -class InvalidCastRule implements \PHPStan\Rules\Rule +final class InvalidCastRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr\Cast::class; + return Node\Expr\Cast::class; } public function processNode(Node $node, Scope $scope): array { - $castTypeCallback = static function (Type $type) use ($node): ?Type { - if ($node instanceof \PhpParser\Node\Expr\Cast\Int_) { - return $type->toInteger(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\Bool_) { - return $type->toBoolean(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\Double) { - return $type->toFloat(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) { - return $type->toString(); + $castTypeCallback = static function (Type $type) use ($node): ?array { + if ($node instanceof Node\Expr\Cast\Int_) { + return [$type->toInteger(), 'int']; + } elseif ($node instanceof Node\Expr\Cast\Bool_) { + return [$type->toBoolean(), 'bool']; + } elseif ($node instanceof Node\Expr\Cast\Double) { + return [$type->toFloat(), 'double']; + } elseif ($node instanceof Node\Expr\Cast\String_) { + return [$type->toString(), 'string']; } return null; @@ -56,20 +55,28 @@ 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; - } + }, ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { 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,8 +91,8 @@ static function (Type $type) use ($castTypeCallback): bool { RuleErrorBuilder::message(sprintf( 'Cannot cast %s to %s.', $scope->getType($node->expr)->describe(VerbosityLevel::value()), - $shortName - ))->line($node->getLine())->build(), + $shortName, + ))->identifier(sprintf('cast.%s', $castIdentifier))->line($node->getStartLine())->build(), ]; } diff --git a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php index 54bb38829a..a2c04dc054 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -4,41 +4,38 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Scalar\Encapsed> + * @implements Rule */ -class InvalidPartOfEncapsedStringRule implements \PHPStan\Rules\Rule +final class InvalidPartOfEncapsedStringRule implements Rule { - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - \PhpParser\PrettyPrinter\Standard $printer, - RuleLevelHelper $ruleLevelHelper + private ExprPrinter $exprPrinter, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->printer = $printer; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\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; } @@ -46,9 +43,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $part, '', - static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - } + static fn (Type $type): bool => !$type->toString() instanceof ErrorType, ); $partType = $typeResult->getType(); if ($partType instanceof ErrorType) { @@ -61,9 +56,9 @@ static function (Type $type): bool { } $messages[] = RuleErrorBuilder::message(sprintf( 'Part %s (%s) of encapsed string cannot be cast to string.', - $this->printer->prettyPrintExpr($part), - $partType->describe(VerbosityLevel::value()) - ))->line($part->getLine())->build(); + $this->exprPrinter->printExpr($part), + $partType->describe(VerbosityLevel::value()), + ))->identifier('encapsedStringPart.nonString')->line($part->getStartLine())->build(); } return $messages; diff --git a/src/Rules/Cast/PrintRule.php b/src/Rules/Cast/PrintRule.php index 3e719af4b5..a8f525c91e 100644 --- a/src/Rules/Cast/PrintRule.php +++ b/src/Rules/Cast/PrintRule.php @@ -10,18 +10,16 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Print_> + * @implements Rule */ -class PrintRule implements Rule +final class PrintRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -35,9 +33,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->expr, '', - static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - } + static fn (Type $type): bool => !$type->toString() instanceof ErrorType, ); if (!$typeResult->getType() instanceof ErrorType @@ -45,8 +41,8 @@ static function (Type $type): bool { ) { return [RuleErrorBuilder::message(sprintf( 'Parameter %s of print cannot be converted to string.', - $typeResult->getType()->describe(VerbosityLevel::value()) - ))->line($node->expr->getLine())->build()]; + $typeResult->getType()->describe(VerbosityLevel::value()), + ))->identifier('print.nonString')->line($node->expr->getStartLine())->build()]; } return []; diff --git a/src/Rules/Cast/UnsetCastRule.php b/src/Rules/Cast/UnsetCastRule.php new file mode 100644 index 0000000000..9d0bdbf724 --- /dev/null +++ b/src/Rules/Cast/UnsetCastRule.php @@ -0,0 +1,40 @@ + + */ +final class UnsetCastRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Expr\Cast\Unset_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->phpVersion->supportsUnsetCast()) { + return []; + } + + return [ + 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 a70b084287..b58a589074 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -2,22 +2,20 @@ namespace PHPStan\Rules; -use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; +use function sprintf; +use function strtolower; -class ClassCaseSensitivityCheck +final class ClassCaseSensitivityCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider, private bool $checkInternalClassCaseSensitivity) { - $this->reflectionProvider = $reflectionProvider; } /** * @param ClassNameNodePair[] $pairs - * @return RuleError[] + * @return list */ public function checkClassNames(array $pairs): array { @@ -28,7 +26,7 @@ public function checkClassNames(array $pairs): array continue; } $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->isBuiltin()) { + if (!$this->checkInternalClassCaseSensitivity && $classReflection->isBuiltin()) { continue; // skip built-in classes } $realClassName = $classReflection->getName(); @@ -39,26 +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(); + $className, + )) + ->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'; - } - - 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 a92f539071..cf39baa35c 100644 --- a/src/Rules/ClassNameNodePair.php +++ b/src/Rules/ClassNameNodePair.php @@ -4,17 +4,11 @@ use PhpParser\Node; -class ClassNameNodePair +final class ClassNameNodePair { - private string $className; - - private Node $node; - - public function __construct(string $className, Node $node) + public function __construct(private string $className, private Node $node) { - $this->className = $className; - $this->node = $node; } public function getClassName(): string diff --git a/src/Rules/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 new file mode 100644 index 0000000000..551f56c464 --- /dev/null +++ b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php @@ -0,0 +1,61 @@ + + */ +final class AccessPrivateConstantThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $constantName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasConstant($constantName)->yes()) { + return []; + } + + $constant = $classType->getConstant($constantName); + if (!$constant->isPrivate()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe access to private constant %s::%s through static::.', + $constant->getDeclaringClass()->getDisplayName(), + $constantName, + ))->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 new file mode 100644 index 0000000000..190be4331b --- /dev/null +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -0,0 +1,69 @@ + + */ +final class ClassAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classLikeNode = $node->getOriginalNode(); + + $errors = $this->attributesCheck->check( + $scope, + $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 new file mode 100644 index 0000000000..71597d271a --- /dev/null +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -0,0 +1,36 @@ + + */ +final class ClassConstantAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->attrGroups, + Attribute::TARGET_CLASS_CONSTANT, + 'class constant', + ); + } + +} diff --git a/src/Rules/Classes/ClassConstantDeclarationRule.php b/src/Rules/Classes/ClassConstantDeclarationRule.php deleted file mode 100644 index a43c6085aa..0000000000 --- a/src/Rules/Classes/ClassConstantDeclarationRule.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ -class ClassConstantDeclarationRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Stmt\ClassConst::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); - - foreach ($node->consts as $const) { - $classReflection->getConstant($const->name->name); - } - - return []; - } - -} diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index a1a517c0d5..a14428d890 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -3,42 +3,45 @@ 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; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ClassConstFetch> + * @implements Rule */ -class ClassConstantRule implements \PHPStan\Rules\Rule +final class ClassConstantRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private ClassNameCheck $classCheck, + private PhpVersion $phpVersion, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -48,41 +51,68 @@ 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))); + } } - $constantName = $node->name->name; + foreach ($constantNameScopes as $constantName => $constantScope) { + $errors = array_merge($errors, $this->processSingleClassConstFetch( + $constantScope, + $node, + (string) $constantName, // @phpstan-ignore cast.useless + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $node, string $constantName): array + { $class = $node->class; $messages = []; - if ($class instanceof \PhpParser\Node\Name) { + if ($class instanceof Node\Name) { $className = (string) $class; $lowercasedClassName = strtolower($className); if (in_array($lowercasedClassName, ['self', 'static'], true)) { 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(), ]; } - $className = $scope->getClassReflection()->getName(); + $classType = $scope->resolveTypeByName($class); } 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(); - if ($currentClassReflection->getParentClass() === false) { + if ($currentClassReflection->getParentClass() === null) { return [ RuleErrorBuilder::message(sprintf( 'Access to parent::%s but %s does not extend any class.', $constantName, - $currentClassReflection->getDisplayName() - ))->build(), + $currentClassReflection->getDisplayName(), + ))->identifier('class.noParent')->build(), ]; } - $className = $currentClassReflection->getParentClass()->getName(); + $classType = $scope->resolveTypeByName($class); } else { if (!$this->reflectionProvider->hasClass($className)) { if ($scope->isInClassExists($className)) { @@ -91,43 +121,90 @@ public function processNode(Node $node, Scope $scope): array if (strtolower($constantName) === 'class') { return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $className))->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) - )->build(), + sprintf('Access to constant %s on an unknown class %s.', $constantName, $className), + ) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } - $className = $this->reflectionProvider->getClass($className)->getName(); + $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 ($scope->isInClass() && $scope->getClassReflection()->getName() === $className) { - $classType = new ThisType($scope->getClassReflection()); - } else { - $classType = new ObjectType($className); + if (strtolower($constantName) === 'class') { + return $messages; } } else { $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $class, - sprintf('Access to constant %s on an unknown class %%s.', $constantName), - static function (Type $type) use ($constantName): bool { - return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(); - } + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class), + sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)), + static fn (Type $type): bool => $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { return $classTypeResult->getUnknownClassErrors(); } + + if (strtolower($constantName) === 'class') { + 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 (!$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(), + ]; + } + } } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if ($classType->isString()->yes()) { return $messages; } @@ -142,12 +219,12 @@ static function (Type $type) use ($constantName): bool { RuleErrorBuilder::message(sprintf( 'Cannot access constant %s on %s.', $constantName, - $typeForDescribe->describe(VerbosityLevel::typeOnly()) - ))->build(), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.nonObject')->build(), ]); } - if (strtolower($constantName) === 'class') { + if (strtolower($constantName) === 'class' || $scope->hasExpressionType($node)->yes()) { return $messages; } @@ -156,8 +233,8 @@ static function (Type $type) use ($constantName): bool { RuleErrorBuilder::message(sprintf( 'Access to undefined constant %s::%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $constantName - ))->build(), + $constantName, + ))->identifier('classConstant.notFound')->build(), ]); } @@ -168,7 +245,10 @@ static function (Type $type) use ($constantName): bool { 'Access to %s constant %s of class %s.', $constantReflection->isPrivate() ? 'private' : 'protected', $constantName, - $constantReflection->getDeclaringClass()->getDisplayName() + $constantReflection->getDeclaringClass()->getDisplayName(), + ))->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 new file mode 100644 index 0000000000..047ce4cee9 --- /dev/null +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -0,0 +1,133 @@ + + */ +final class DuplicateDeclarationRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + $identifierType = strtolower($classReflection->getClassTypeDescription()); + + $errors = []; + + $declaredClassConstantsOrEnumCases = []; + foreach ($node->getOriginalNode()->stmts as $stmtNode) { + if ($stmtNode instanceof EnumCase) { + if (array_key_exists($stmtNode->name->name, $declaredClassConstantsOrEnumCases)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare enum case %s::%s.', + $classReflection->getDisplayName(), + $stmtNode->name->name, + ))->identifier(sprintf('%s.duplicateEnumCase', $identifierType)) + ->line($stmtNode->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true; + } + } elseif ($stmtNode instanceof ClassConst) { + foreach ($stmtNode->consts as $classConstNode) { + if (array_key_exists($classConstNode->name->name, $declaredClassConstantsOrEnumCases)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare constant %s::%s.', + $classReflection->getDisplayName(), + $classConstNode->name->name, + ))->identifier(sprintf('%s.duplicateConstant', $identifierType)) + ->line($classConstNode->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true; + } + } + } + } + + $declaredProperties = []; + foreach ($node->getOriginalNode()->getProperties() as $propertyDecl) { + foreach ($propertyDecl->props as $property) { + if (array_key_exists($property->name->name, $declaredProperties)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare property %s::$%s.', + $classReflection->getDisplayName(), + $property->name->name, + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($property->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $declaredProperties[$property->name->name] = true; + } + } + } + + $declaredFunctions = []; + foreach ($node->getOriginalNode()->getMethods() as $method) { + if ($method->name->toLowerString() === '__construct') { + foreach ($method->params as $param) { + if ($param->flags === 0) { + continue; + } + + if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { + throw new ShouldNotHappenException(); + } + + $propertyName = $param->var->name; + + if (array_key_exists($propertyName, $declaredProperties)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare property %s::$%s.', + $classReflection->getDisplayName(), + $propertyName, + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($param->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $declaredProperties[$propertyName] = true; + } + } + } + if (array_key_exists(strtolower($method->name->name), $declaredFunctions)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare method %s::%s().', + $classReflection->getDisplayName(), + $method->name->name, + ))->identifier(sprintf('%s.duplicateMethod', $identifierType)) + ->line($method->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $declaredFunctions[strtolower($method->name->name)] = true; + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php new file mode 100644 index 0000000000..51c07fdfd8 --- /dev/null +++ b/src/Rules/Classes/EnumSanityRule.php @@ -0,0 +1,226 @@ + + */ +final class EnumSanityRule implements Rule +{ + + private const ALLOWED_MAGIC_METHODS = [ + '__call' => true, + '__callstatic' => true, + '__invoke' => true, + ]; + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isEnum()) { + return []; + } + + /** @var Node\Stmt\Enum_ $enumNode */ + $enumNode = $node->getOriginalNode(); + + $errors = []; + + foreach ($enumNode->getMethods() as $methodNode) { + $lowercasedMethodName = $methodNode->name->toLowerString(); + if ($methodNode->isMagic()) { + if ($lowercasedMethodName === '__construct') { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s contains constructor.', + $classReflection->getDisplayName(), + )) + ->identifier('enum.constructor') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); + } elseif ($lowercasedMethodName === '__destruct') { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s contains destructor.', + $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().', + $classReflection->getDisplayName(), + $methodNode->name->name, + )) + ->identifier('enum.magicMethod') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); + } + } + + if ($lowercasedMethodName === 'cases') { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s cannot redeclare native method %s().', + $classReflection->getDisplayName(), + $methodNode->name->name, + )) + ->identifier('enum.methodRedeclaration') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); + } + + if ($enumNode->scalarType === null) { + continue; + } + + if (!in_array($lowercasedMethodName, ['from', 'tryfrom'], true)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s cannot redeclare native method %s().', + $classReflection->getDisplayName(), + $methodNode->name->name, + )) + ->identifier('enum.methodRedeclaration') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); + } + + if ( + $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.', + $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 b23c0a5bd1..9bd5e0ef5e 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -5,27 +5,25 @@ 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; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ -class ExistingClassInClassExtendsRule implements \PHPStan\Rules\Rule +final class ExistingClassInClassExtendsRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassNameCheck $classCheck, + private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -39,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()->build(); + $extendedClassName, + )) + ->identifier('class.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedClassName); @@ -58,20 +71,70 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s extends interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->identifier('class.extendsInterface') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->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(), + )) + ->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', - $extendedClassName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->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(), + )) + ->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 5cf38c106f..7a4d4d21eb 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -6,31 +6,30 @@ 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; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Instanceof_> + * @implements Rule */ -class ExistingClassInInstanceOfRule implements \PHPStan\Rules\Rule +final class ExistingClassInInstanceOfRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private bool $checkClassCaseSensitivity, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string @@ -41,7 +40,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $class = $node->class; - if (!($class instanceof \PhpParser\Node\Name)) { + if (!($class instanceof Node\Name)) { return []; } @@ -55,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())->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 12cb359287..08524a3fd4 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -5,51 +5,57 @@ 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; +use function array_map; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\TraitUse> + * @implements Rule */ -class ExistingClassInTraitUseRule implements \PHPStan\Rules\Rule +final class ExistingClassInTraitUseRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassNameCheck $classCheck, + private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\TraitUse::class; + return Node\Stmt\TraitUse::class; } public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (Node\Name $traitName): ClassNameNodePair { - return new ClassNameNodePair((string) $traitName, $traitName); - }, $node->traits) - ); - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + 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 { @@ -65,13 +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()->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, $traitName))->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, $traitName))->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())) + ->identifier('traitUse.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index 9c0b4ae69b..b1e639af29 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -5,27 +5,26 @@ 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; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ -class ExistingClassesInClassImplementsRule implements \PHPStan\Rules\Rule +final class ExistingClassesInClassImplementsRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassNameCheck $classCheck, + private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -35,26 +34,36 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (Node\Name $interfaceName): ClassNameNodePair { - return new ClassNameNodePair((string) $interfaceName, $interfaceName); - }, $node->implements) - ); - $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()->build(); + $implementedClassName, + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); @@ -62,14 +71,29 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s implements class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $implementedClassName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->identifier('classImplements.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s implements trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $implementedClassName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->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(), + )) + ->identifier('classImplements.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php new file mode 100644 index 0000000000..1a7f8b2883 --- /dev/null +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -0,0 +1,100 @@ + + */ +final class ExistingClassesInEnumImplementsRule implements Rule +{ + + public function __construct( + private ClassNameCheck $classCheck, + private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Enum_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $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, + ]), + ); + + foreach ($node->implements as $implements) { + $implementedClassName = (string) $implements; + if (!$this->reflectionProvider->hasClass($implementedClassName)) { + if (!$scope->isInClassExists($implementedClassName)) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Enum %s implements unknown interface %s.', + $currentEnumName, + $implementedClassName, + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); + } + } else { + $reflection = $this->reflectionProvider->getClass($implementedClassName); + if ($reflection->isClass()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements class %s.', + $currentEnumName, + $reflection->getDisplayName(), + )) + ->identifier('enumImplements.class') + ->nonIgnorable() + ->build(); + } elseif ($reflection->isTrait()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements trait %s.', + $currentEnumName, + $reflection->getDisplayName(), + )) + ->identifier('enumImplements.trait') + ->nonIgnorable() + ->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements enum %s.', + $currentEnumName, + $reflection->getDisplayName(), + )) + ->identifier('enumImplements.enum') + ->nonIgnorable() + ->build(); + } + } + } + + return $messages; + } + +} diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index 4847172050..8d0d071471 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -5,27 +5,26 @@ 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; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> + * @implements Rule */ -class ExistingClassesInInterfaceExtendsRule implements \PHPStan\Rules\Rule +final class ExistingClassesInInterfaceExtendsRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassNameCheck $classCheck, + private ReflectionProvider $reflectionProvider, + private bool $discoveringSymbolsTip, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -35,22 +34,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (Node\Name $interfaceName): ClassNameNodePair { - return new ClassNameNodePair((string) $interfaceName, $interfaceName); - }, $node->extends) + $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()->build(); + $extendedInterfaceName, + )) + ->identifier('interface.notFound') + ->nonIgnorable(); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $messages[] = $errorBuilder->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedInterfaceName); @@ -58,14 +67,29 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends class %s.', $currentInterfaceName, - $extendedInterfaceName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->identifier('interfaceExtends.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends trait %s.', $currentInterfaceName, - $extendedInterfaceName - ))->nonIgnorable()->build(); + $reflection->getDisplayName(), + )) + ->identifier('interfaceExtends.trait') + ->nonIgnorable() + ->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Interface %s extends enum %s.', + $currentInterfaceName, + $reflection->getDisplayName(), + )) + ->identifier('interfaceExtends.enum') + ->nonIgnorable() + ->build(); } } diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index 02e3e4c63f..a4b3cfe70c 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ObjectType; @@ -11,24 +13,20 @@ use PHPStan\Type\StringType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Instanceof_> + * @implements Rule */ -class ImpossibleInstanceOfRule implements \PHPStan\Rules\Rule +final class ImpossibleInstanceOfRule implements Rule { - private bool $checkAlwaysTrueInstanceof; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - bool $checkAlwaysTrueInstanceof, - bool $treatPhpDocTypesAsCertain + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->checkAlwaysTrueInstanceof = $checkAlwaysTrueInstanceof; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -38,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() + 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()), - $classType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $scope->getType($node->expr)->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::typeOnly()), + ))->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 new file mode 100644 index 0000000000..019d6a2979 --- /dev/null +++ b/src/Rules/Classes/InstantiationCallableRule.php @@ -0,0 +1,32 @@ + + */ +final class InstantiationCallableRule implements Rule +{ + + public function getNodeType(): string + { + return InstantiationCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message('Cannot create callable from the new operator.') + ->identifier('callable.notSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 8b504943b8..6c5ef87c20 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -5,39 +5,40 @@ use PhpParser\Node; 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\RuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; 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; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ -class InstantiationRule implements \PHPStan\Rules\Rule +final class InstantiationRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $check, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $check, + private ClassNameCheck $classCheck, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -48,19 +49,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $errors = []; - foreach ($this->getClassNames($node, $scope) as $class) { - $errors = array_merge($errors, $this->checkClassName($class, $node, $scope)); + foreach ($this->getClassNames($node, $scope) as [$class, $isName]) { + $errors = array_merge($errors, $this->checkClassName($class, $isName, $node, $scope)); } return $errors; } /** - * @param string $class - * @param \PhpParser\Node\Expr\New_ $node - * @param Scope $scope - * @return RuleError[] + * @param Node\Expr\New_ $node + * @return list */ - private function checkClassName(string $class, Node $node, Scope $scope): array + private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array { $lowercasedClass = strtolower($class); $messages = []; @@ -68,7 +67,9 @@ private function checkClassName(string $class, Node $node, Scope $scope): array 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(), ]; } @@ -92,24 +93,28 @@ private function checkClassName(string $class, Node $node, Scope $scope): array } 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() === false) { + if ($scope->getClassReflection()->getParentClass() === null) { return [ RuleErrorBuilder::message(sprintf( '%s::%s() calls new parent but %s does not extend any class.', $scope->getClassReflection()->getDisplayName(), $scope->getFunctionName(), - $scope->getClassReflection()->getDisplayName() - ))->build(), + $scope->getClassReflection()->getDisplayName(), + ))->identifier('class.noParent')->build(), ]; } $classReflection = $scope->getClassReflection()->getParentClass(); @@ -119,41 +124,60 @@ private function checkClassName(string $class, Node $node, Scope $scope): array 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))->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); } - if (!$isStatic && $classReflection->isInterface()) { + if ($classReflection->isEnum() && $isName) { return [ RuleErrorBuilder::message( - sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()) - )->build(), + sprintf('Cannot instantiate enum %s.', $classReflection->getDisplayName()), + )->identifier('new.enum')->build(), ]; } - if (!$isStatic && $classReflection->isAbstract()) { + if (!$isStatic && $classReflection->isInterface() && $isName) { return [ RuleErrorBuilder::message( - sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()) - )->build(), + sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()), + )->identifier('new.interface')->build(), ]; } + if (!$isStatic && $classReflection->isAbstract() && $isName) { + return [ + RuleErrorBuilder::message( + sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()), + )->identifier('new.abstract')->build(), + ]; + } + + if (!$isName) { + return []; + } + if (!$classReflection->hasConstructor()) { - if (count($node->args) > 0) { + if (count($node->getArgs()) > 0) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Class %s does not have a constructor and must be instantiated without any parameters.', - $classReflection->getDisplayName() - ))->build(), + $classReflection->getDisplayName(), + ))->identifier('new.noConstructor')->build(), ]); } @@ -167,63 +191,91 @@ private function checkClassName(string $class, Node $node, Scope $scope): array $classReflection->getDisplayName(), $constructorReflection->isPrivate() ? 'private' : 'protected', $constructorReflection->getDeclaringClass()->getDisplayName(), - $constructorReflection->getName() - ))->build(); + $constructorReflection->getName(), + )) + ->identifier(sprintf('new.%sConstructor', $constructorReflection->isPrivate() ? 'private' : 'protected')) + ->build(); } + $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); + return array_merge($messages, $this->check->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $constructorReflection->getVariants() + $node->getArgs(), + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ), $scope, + $constructorReflection->getDeclaringClass()->isBuiltin(), $node, - [ - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter #%d %s of class ' . $classReflection->getDisplayName() . ' constructor expects %s, %s given.', - '', // constructor does not have a return type - 'Parameter #%d %s of class ' . $classReflection->getDisplayName() . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of class ' . $classReflection->getDisplayName(), - ] + '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 \PhpParser\Node\Expr\New_ $node $node - * @param Scope $scope - * @return string[] + * @param Node\Expr\New_ $node + * @return array */ private function getClassNames(Node $node, Scope $scope): array { - if ($node->class instanceof \PhpParser\Node\Name) { - return [(string) $node->class]; + if ($node->class instanceof Node\Name) { + return [[(string) $node->class, true]]; } if ($node->class instanceof Node\Stmt\Class_) { - $anonymousClassType = $scope->getType($node); - if (!$anonymousClassType instanceof TypeWithClassName) { - throw new \PHPStan\ShouldNotHappenException(); + $classNames = $scope->getType($node)->getObjectClassNames(); + if ($classNames === []) { + throw new ShouldNotHappenException(); } - return [$anonymousClassType->getClassName()]; + 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 function (ConstantStringType $type): string { - return $type->getValue(); - }, - TypeUtils::getConstantStrings($type) + static fn (ConstantStringType $type): array => [$type->getValue(), true], + $type->getConstantStrings(), + ), + array_map( + static fn (string $name): array => [$name, false], + $type->getObjectClassNames(), ), - TypeUtils::getDirectClassNames($type) ); } diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php new file mode 100644 index 0000000000..6472285f76 --- /dev/null +++ b/src/Rules/Classes/InvalidPromotedPropertiesRule.php @@ -0,0 +1,103 @@ + + */ +final class InvalidPromotedPropertiesRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $hasPromotedProperties = false; + + foreach ($node->getParams() as $param) { + if ($param->flags !== 0) { + $hasPromotedProperties = true; + break; + } + + if ($param->hooks === []) { + continue; + } + + $hasPromotedProperties = true; + break; + } + + if (!$hasPromotedProperties) { + return []; + } + + if (!$this->phpVersion->supportsPromotedProperties()) { + return [ + RuleErrorBuilder::message( + 'Promoted properties are supported only on PHP 8.0 and later.', + )->identifier('property.promotedNotSupported')->nonIgnorable()->build(), + ]; + } + + if ( + !$node instanceof Node\Stmt\ClassMethod + || ( + $node->name->toLowerString() !== '__construct' + && $node->getAttribute('originalTraitMethodName') !== '__construct') + ) { + return [ + RuleErrorBuilder::message( + 'Promoted properties can be in constructor only.', + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), + ]; + } + + if ($node->getStmts() === null) { + return [ + RuleErrorBuilder::message( + 'Promoted properties are not allowed in abstract constructors.', + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), + ]; + } + + $errors = []; + foreach ($node->getParams() as $param) { + if ($param->flags === 0) { + continue; + } + + if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { + throw new ShouldNotHappenException(); + } + + if (!$param->variadic) { + continue; + } + + $propertyName = $param->var->name; + $errors[] = RuleErrorBuilder::message( + sprintf('Promoted property parameter $%s can not be variadic.', $propertyName), + )->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 new file mode 100644 index 0000000000..9621d3c9ec --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -0,0 +1,30 @@ + + */ +final class LocalTypeAliasesRule implements Rule +{ + + public function __construct(private LocalTypeAliasesCheck $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/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 4b2d5ac413..de75fc13a6 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -4,127 +4,27 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; -use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; -use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ErrorType; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\NeverType; -use PHPStan\Type\VerbosityLevel; /** - * @implements Rule + * @implements Rule */ -class MixinRule implements Rule +final class MixinRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private MissingTypehintCheck $missingTypehintCheck; - - private bool $checkClassCaseSensitivity; - - public function __construct( - FileTypeMapper $fileTypeMapper, - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - MissingTypehintCheck $missingTypehintCheck, - bool $checkClassCaseSensitivity - ) + public function __construct(private MixinCheck $check) { - $this->fileTypeMapper = $fileTypeMapper; - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->missingTypehintCheck = $missingTypehintCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string { - return Node\Stmt\Class_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - // anonymous class - return []; - } - - $className = (string) $node->namespacedName; - $docComment = $node->getDocComment(); - if ($docComment === null) { - return []; - } - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $className, - null, - null, - $docComment->getText() - ); - $mixinTags = $resolvedPhpDoc->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 ( - $type instanceof ErrorType - || ($type instanceof NeverType && !$type->isExplicit()) - ) { - $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 class %s is not generic.', - 'Generic type %s in PHPDoc tag @mixin does not specify all template types of class %s: %s', - 'Generic type %s in PHPDoc tag @mixin specifies %d template types, but class %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @mixin is not subtype of template type %s of class %s.' - )); - - 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))->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 f77485cad8..5540646616 100644 --- a/src/Rules/Classes/NewStaticRule.php +++ b/src/Rules/Classes/NewStaticRule.php @@ -7,11 +7,12 @@ use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ -class NewStaticRule implements Rule +final class NewStaticRule implements Rule { public function getNodeType(): string @@ -40,7 +41,8 @@ public function processNode(Node $node, Scope $scope): array $messages = [ RuleErrorBuilder::message('Unsafe usage of new static().') - ->tip('Consider making the class or the constructor final.') + ->identifier('new.static') + ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static') ->build(), ]; if (!$classReflection->hasConstructor()) { @@ -52,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 new file mode 100644 index 0000000000..24f6850492 --- /dev/null +++ b/src/Rules/Classes/NonClassAttributeClassRule.php @@ -0,0 +1,83 @@ + + */ +final class NonClassAttributeClassRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + foreach ($originalNode->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $name = $attr->name->toLowerString(); + if ($name === 'attribute') { + return $this->check($scope); + } + } + } + + return []; + } + + /** + * @return list + */ + private function check(Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $classReflection = $scope->getClassReflection(); + if (!$classReflection->isClass()) { + return [ + RuleErrorBuilder::message(sprintf( + '%s cannot be an Attribute class.', + $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())) + ->identifier('attribute.abstract') + ->build(), + ]; + } + + if (!$classReflection->hasConstructor()) { + return []; + } + + if (!$classReflection->getConstructor()->isPublic()) { + return [ + RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName())) + ->identifier('attribute.constructorNotPublic') + ->build(), + ]; + } + + return []; + } + +} 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..88c0b88ccd --- /dev/null +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -0,0 +1,86 @@ + + */ +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(); + foreach ($type->getObjectClassNames() as $className) { + if ($classReflection->is($className)) { + 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(); + + break; + } + } + } + + foreach ($classReflection->getTraits(true) as $trait) { + $extendsTags = $trait->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + foreach ($type->getObjectClassNames() as $className) { + if ($classReflection->is($className)) { + 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(); + + break; + } + } + } + + 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 new file mode 100644 index 0000000000..c73a1f8340 --- /dev/null +++ b/src/Rules/Classes/TraitAttributeClassRule.php @@ -0,0 +1,39 @@ + + */ +final class TraitAttributeClassRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $name = $attr->name->toLowerString(); + if ($name === 'attribute') { + return [ + RuleErrorBuilder::message('Trait cannot be an Attribute class.') + ->identifier('attribute.trait') + ->build(), + ]; + } + } + } + + return []; + } + +} diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index 9b234eb6ff..b29ca65bd4 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -5,61 +5,73 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; -use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassMethodNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\ShouldNotHappenException; +use function array_filter; +use function array_map; +use function array_values; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ClassMethod> + * @implements Rule */ -class UnusedConstructorParametersRule implements \PHPStan\Rules\Rule +final class UnusedConstructorParametersRule implements Rule { - private \PHPStan\Rules\UnusedFunctionParametersCheck $check; - - public function __construct(UnusedFunctionParametersCheck $check) + public function __construct(private UnusedFunctionParametersCheck $check) { - $this->check = $check; } public function getNodeType(): string { - return ClassMethod::class; + return InClassMethodNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + $method = $node->getMethodReflection(); + $originalNode = $node->getOriginalNode(); + if (!$method->isConstructor() || $originalNode->stmts === null) { + return []; } - if ($node->name->name !== '__construct' || $node->stmts === null) { + if (count($originalNode->params) === 0) { return []; } - - if (count($node->params) === 0) { + if ($node->getClassReflection()->isAttributeClass()) { return []; } + foreach ($node->getClassReflection()->getInterfaces() as $interface) { + if ($interface->hasConstructor()) { + return []; + } + } + $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - $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)) { - throw new \PHPStan\ShouldNotHappenException(); + array_map(static function (Param $parameter): Variable { + if (!$parameter->var instanceof Variable) { + throw new ShouldNotHappenException(); } - return $parameter->var->name; - }, $node->params), - $node->stmts, + return $parameter->var; + }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), + $originalNode->stmts, $message, - 'constructor.unusedParameter' + 'constructor.unusedParameter', ); } diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index d8ef06a110..013e6b4b81 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -2,106 +2,154 @@ namespace PHPStan\Rules\Comparison; +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; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp\BooleanAnd> + * @implements Rule */ -class BooleanAndConstantConditionRule implements \PHPStan\Rules\Rule +final class BooleanAndConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\BinaryOp\BooleanAnd::class; + return BooleanAndNode::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $errors = []; - $leftType = $this->helper->getBooleanType($scope, $node->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $originalNode = $node->getOriginalNode(); + $nodeText = $originalNode->getOperatorSigil(); + $leftType = $this->helper->getBooleanType($scope, $originalNode->left); + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $originalNode->left); 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($node->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(); $rightType = $this->helper->getBooleanType( - $scope->filterByTruthyValue($node->left), - $node->right + $rightScope, + $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $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( - $scope->doNotTreatPhpDocTypesAsCertain()->filterByTruthyValue($node->left), - $node->right + $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($node->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($node); + 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, $node, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($node); + $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 f87f3d342b..2b04d48f80 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -2,36 +2,37 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BooleanNot> + * @implements Rule */ -class BooleanNotConstantConditionRule implements \PHPStan\Rules\Rule +final class BooleanNotConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\BooleanNot::class; + return Node\Expr\BooleanNot::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->expr); @@ -45,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(), - ]; + $exprType->getValue() ? 'false' : 'true', + )))->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 b079e3f8b8..b991f45981 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -2,105 +2,154 @@ namespace PHPStan\Rules\Comparison; +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; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp\BooleanOr> + * @implements Rule */ -class BooleanOrConstantConditionRule implements \PHPStan\Rules\Rule +final class BooleanOrConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\BinaryOp\BooleanOr::class; + return BooleanOrNode::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { + $originalNode = $node->getOriginalNode(); + $nodeText = $originalNode->getOperatorSigil(); $messages = []; - $leftType = $this->helper->getBooleanType($scope, $node->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $leftType = $this->helper->getBooleanType($scope, $originalNode->left); + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $originalNode->left); 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($node->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(); $rightType = $this->helper->getBooleanType( - $scope->filterByFalseyValue($node->left), - $node->right + $rightScope, + $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $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( - $scope->doNotTreatPhpDocTypesAsCertain()->filterByFalseyValue($node->left), - $node->right + $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($node->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($node); + 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, $node, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($node); + $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 18de49a6c7..36b2c569d8 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -8,33 +8,25 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\BooleanType; -class ConstantConditionRuleHelper +final class ConstantConditionRuleHelper { - private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - } - - public function shouldReportAlwaysTrueByDefault(Expr $expr): bool - { - return $expr instanceof Expr\BooleanNot - || $expr instanceof Expr\BinaryOp\BooleanOr - || $expr instanceof Expr\BinaryOp\BooleanAnd - || $expr instanceof Expr\Ternary - || $expr instanceof Expr\Isset_; } 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 @@ -44,6 +36,7 @@ public function shouldSkip(Scope $scope, Expr $expr): bool || $expr instanceof Expr\BinaryOp\BooleanAnd || $expr instanceof Expr\Ternary || $expr instanceof Expr\Isset_ + || $expr instanceof Expr\Empty_ || $expr instanceof Expr\BinaryOp\Greater || $expr instanceof Expr\BinaryOp\GreaterOrEqual || $expr instanceof Expr\BinaryOp\Smaller diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php new file mode 100644 index 0000000000..b7d3fe977d --- /dev/null +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -0,0 +1,90 @@ + + */ +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->isTrue()->yes() && !$nodeType->isFalse()->yes()) { + return []; + } + + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); + if ($instanceofTypeWithoutPhpDocs->isTrue()->yes() || $instanceofTypeWithoutPhpDocs->isFalse()->yes()) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + if ($nodeType->isFalse()->yes()) { + 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 new file mode 100644 index 0000000000..0de2772219 --- /dev/null +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -0,0 +1,95 @@ + + */ +final class DoWhileLoopConstantConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return DoWhileLoopConditionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $exprType = $this->helper->getBooleanType($scope, $node->getCond()); + if ($exprType instanceof ConstantBooleanType) { + if ($exprType->getValue()) { + foreach ($node->getExitPoints() as $exitPoint) { + $statement = $exitPoint->getStatement(); + if ($statement instanceof Break_) { + return []; + } + if (!$statement instanceof Continue_) { + return []; + } + if ($statement->num === null) { + continue; + } + if (!$statement->num instanceof Int_) { + continue; + } + $value = $statement->num->value; + if ($value === 1) { + continue; + } + + if ($value > 1) { + return []; + } + } + } + + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->getCond()); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + return [ + $addTip(RuleErrorBuilder::message(sprintf( + 'Do-while loop condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + ))) + ->line($node->getCond()->getStartLine()) + ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 194b7ed1e2..a3c0bfab64 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -2,36 +2,37 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ElseIf_> + * @implements Rule */ -class ElseIfConstantConditionRule implements \PHPStan\Rules\Rule +final class ElseIfConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\ElseIf_::class; + return Node\Stmt\ElseIf_::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -45,15 +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())->build(), - ]; + $exprType->getValue() ? 'true' : 'false', + )))->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 d37cf65823..f0d71c3e01 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -2,36 +2,35 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\If_> + * @implements Rule */ -class IfConstantConditionRule implements \PHPStan\Rules\Rule +final class IfConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\If_::class; + return Node\Stmt\If_::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -45,15 +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())->build(), + $exprType->getValue() ? 'true' : 'false', + ))) + ->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 f873a37332..a4b522d9f3 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -4,34 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class ImpossibleCheckTypeFunctionCallRule implements \PHPStan\Rules\Rule +final class ImpossibleCheckTypeFunctionCallRule implements Rule { - private \PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $checkAlwaysTrueCheckTypeFunctionCall; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $checkAlwaysTrueCheckTypeFunctionCall, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\FuncCall::class; + return Node\Expr\FuncCall::class; } public function processNode(Node $node, Scope $scope): array @@ -41,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 []; @@ -58,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) { @@ -67,20 +62,28 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Call to function %s()%s will always evaluate to false.', $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) - )))->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->args) - )))->build(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->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 62fa9185f5..5cba66c18e 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -2,88 +2,97 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; use PhpParser\Node\Arg; 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\ErrorType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeCombinator; +use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; - -class ImpossibleCheckTypeHelper +use function array_map; +use function array_pop; +use function count; +use function implode; +use function in_array; +use function is_string; +use function sprintf; +use function strtolower; + +final class ImpossibleCheckTypeHelper { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - /** @var string[] */ - private array $universalObjectCratesClasses; - - private bool $treatPhpDocTypesAsCertain; - /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Analyser\TypeSpecifier $typeSpecifier * @param string[] $universalObjectCratesClasses - * @param bool $treatPhpDocTypesAsCertain */ public function __construct( - ReflectionProvider $reflectionProvider, - TypeSpecifier $typeSpecifier, - array $universalObjectCratesClasses, - bool $treatPhpDocTypesAsCertain + private ReflectionProvider $reflectionProvider, + private TypeSpecifier $typeSpecifier, + private array $universalObjectCratesClasses, + private bool $treatPhpDocTypesAsCertain, ) { - $this->reflectionProvider = $reflectionProvider; - $this->typeSpecifier = $typeSpecifier; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function findSpecifiedType( Scope $scope, - Expr $node + Expr $node, ): ?bool { - if ( - $node instanceof FuncCall - && count($node->args) > 0 - ) { - if ($node->name instanceof \PhpParser\Node\Name) { + if ($node instanceof FuncCall) { + if ($node->isFirstClassCallable()) { + return null; + } + $argsCount = count($node->getArgs()); + if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); + if ($functionName === 'assert' && $argsCount >= 1) { + $arg = $node->getArgs()[0]->value; + $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); + $assertValueIsTrue = $assertValue->isTrue()->yes(); + if (! $assertValueIsTrue && ! $assertValue->isFalse()->yes()) { + return null; + } + + return $assertValueIsTrue; + } if (in_array($functionName, [ 'class_exists', 'interface_exists', 'trait_exists', + 'enum_exists', ], true)) { return null; } - if ($functionName === 'count') { + if (in_array($functionName, ['count', 'sizeof'], true)) { return null; - } elseif ($functionName === 'is_numeric') { - $argType = $scope->getType($node->args[0]->value); - if (count(TypeUtils::getConstantScalars($argType)) > 0) { - return !$argType->toNumber() instanceof ErrorType; - } } elseif ($functionName === 'defined') { return null; - } elseif ( - $functionName === 'in_array' - && count($node->args) >= 3 - ) { - $haystackType = $scope->getType($node->args[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; } @@ -92,22 +101,68 @@ public function findSpecifiedType( return null; } - if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { - $needleType = $scope->getType($node->args[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($needleType->getFiniteTypes()); + $constantHaystackTypesCount = count($valueType->getFiniteTypes()); + $isNeedleSupertype = $needleType->isSuperTypeOf($valueType); + if ($haystackType->isConstantArray()->no()) { + if ($haystackType->isIterableAtLeastOnce()->yes()) { + // In this case the generic implementation via typeSpecifier fails, because the argument types cannot be narrowed down. + if ($constantNeedleTypesCount === 1 && $constantHaystackTypesCount === 1) { + if ($isNeedleSupertype->yes()) { + return true; + } + if ($isNeedleSupertype->no()) { + return false; + } + } - $haystackArrayTypes = TypeUtils::getArrays($haystackType); - if (count($haystackArrayTypes) === 1 && $haystackArrayTypes[0]->getIterableValueType() instanceof NeverType) { return null; } + } - $valueType = $haystackType->getIterableValueType(); - $isNeedleSupertype = $needleType->isSuperTypeOf($valueType); + if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { + $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; + } } } @@ -116,22 +171,19 @@ public function findSpecifiedType( } if ($isNeedleSupertype->yes()) { - $hasConstantNeedleTypes = count(TypeUtils::getConstantScalars($needleType)) > 0; - $hasConstantHaystackTypes = count(TypeUtils::getConstantScalars($valueType)) > 0; + $hasConstantNeedleTypes = $constantNeedleTypesCount > 0; + $hasConstantHaystackTypes = $constantHaystackTypesCount > 0; if ( - ( - !$hasConstantNeedleTypes - && !$hasConstantHaystackTypes - ) + (!$hasConstantNeedleTypes && !$hasConstantHaystackTypes) || $hasConstantNeedleTypes !== $hasConstantHaystackTypes ) { return null; } } } - } elseif ($functionName === 'method_exists' && count($node->args) >= 2) { - $objectType = $scope->getType($node->args[0]->value); - $methodType = $scope->getType($node->args[1]->value); + } elseif ($functionName === 'method_exists' && $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()) @@ -139,12 +191,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; } @@ -153,50 +208,88 @@ 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()) { + return null; + } + $sureTypes = $specifiedTypes->getSureTypes(); $sureNotTypes = $specifiedTypes->getSureNotTypes(); - $isSpecified = static function (Expr $expr) use ($scope, $node): bool { - return ( - $node instanceof FuncCall - || $node instanceof MethodCall - || $node instanceof Expr\StaticCall - ) && $scope->isSpecified($expr); - }; - - if (count($sureTypes) === 1 && count($sureNotTypes) === 0) { - $sureType = reset($sureTypes); - if ($isSpecified($sureType[0])) { + $rootExpr = $specifiedTypes->getRootExpr(); + if ($rootExpr !== null) { + if (self::isSpecified($typeSpecifierScope, $node, $rootExpr)) { return null; } + $rootExprType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr)); + if ($rootExprType instanceof ConstantBooleanType) { + return $rootExprType->getValue(); + } + + return null; + } + + $results = []; + + foreach ($sureTypes as $sureType) { + if (self::isSpecified($typeSpecifierScope, $node, $sureType[0])) { + $results[] = TrinaryLogic::createMaybe(); + continue; + } + if ($this->treatPhpDocTypesAsCertain) { $argumentType = $scope->getType($sureType[0]); } else { $argumentType = $scope->getNativeType($sureType[0]); } - /** @var \PHPStan\Type\Type $resultType */ + /** @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) { @@ -205,67 +298,58 @@ public function findSpecifiedType( $argumentType = $scope->getNativeType($sureNotType[0]); } - /** @var \PHPStan\Type\Type $resultType */ + /** @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); + } + + if ($expr instanceof Expr\BinaryOp) { + return self::isSpecified($scope, $node, $expr->left) || self::isSpecified($scope, $node, $expr->right); } - return null; + return ( + $node instanceof FuncCall + || $node instanceof MethodCall + || $node instanceof Expr\StaticCall + ) && $scope->hasExpressionType($expr)->yes(); } /** - * @param Scope $scope - * @param \PhpParser\Node\Arg[] $args - * @return string + * @param Node\Arg[] $args */ public function getArgumentsDescription( Scope $scope, - array $args + array $args, ): string { if (count($args) === 0) { return ''; } - $descriptions = array_map(static function (Arg $arg) use ($scope): string { - return $scope->getType($arg->value)->describe(VerbosityLevel::value()); - }, $args); + $descriptions = array_map(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)); @@ -276,7 +360,7 @@ public function getArgumentsDescription( return sprintf( ' with arguments %s and %s', implode(', ', $descriptions), - $lastDescription + $lastDescription, ); } @@ -290,8 +374,50 @@ public function doNotTreatPhpDocTypesAsCertain(): self $this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, - false + false, ); } + 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 db711eeca7..4b79d98acb 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -5,35 +5,31 @@ 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; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall> + * @implements Rule */ -class ImpossibleCheckTypeMethodCallRule implements \PHPStan\Rules\Rule +final class ImpossibleCheckTypeMethodCallRule implements Rule { - private \PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $checkAlwaysTrueCheckTypeFunctionCall; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $checkAlwaysTrueCheckTypeFunctionCall, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\MethodCall::class; + return Node\Expr\MethodCall::class; } public function processNode(Node $node, Scope $scope): array @@ -56,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) { @@ -67,36 +66,45 @@ public function processNode(Node $node, Scope $scope): array 'Call to method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) - )))->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->args) - )))->build(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->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( Expr $var, string $methodName, - Scope $scope + Scope $scope, ): MethodReflection { $calledOnType = $scope->getType($var); - if (!$calledOnType->hasMethod($methodName)->yes()) { - throw new \PHPStan\ShouldNotHappenException(); + $method = $scope->getMethodReflection($calledOnType, $methodName); + if ($method === null) { + throw new ShouldNotHappenException(); } - return $calledOnType->getMethod($methodName, $scope); + return $method; } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index e49eb8d430..e4b3721538 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -5,36 +5,31 @@ 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; -use PHPStan\Type\ObjectType; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticCall> + * @implements Rule */ -class ImpossibleCheckTypeStaticMethodCallRule implements \PHPStan\Rules\Rule +final class ImpossibleCheckTypeStaticMethodCallRule implements Rule { - private \PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $checkAlwaysTrueCheckTypeFunctionCall; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $checkAlwaysTrueCheckTypeFunctionCall, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\StaticCall::class; + return Node\Expr\StaticCall::class; } public function processNode(Node $node, Scope $scope): array @@ -57,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) { @@ -69,49 +67,54 @@ public function processNode(Node $node, Scope $scope): array 'Call to static method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) - )))->build(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->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->args) - )))->build(), - ]; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $method = $this->getMethod($node->class, $node->name->name, $scope); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to static method %s::%s()%s will always evaluate to true.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - return []; + $errorBuilder->identifier('staticMethod.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } /** * @param Node\Name|Expr $class - * @param string $methodName - * @param Scope $scope - * @return MethodReflection - * @throws \PHPStan\ShouldNotHappenException + * @throws ShouldNotHappenException */ private function getMethod( $class, string $methodName, - Scope $scope + Scope $scope, ): MethodReflection { if ($class instanceof Node\Name) { - $calledOnType = new ObjectType($scope->resolveName($class)); + $calledOnType = $scope->resolveTypeByName($class); } else { $calledOnType = $scope->getType($class); } - if (!$calledOnType->hasMethod($methodName)->yes()) { - throw new \PHPStan\ShouldNotHappenException(); + $method = $scope->getMethodReflection($calledOnType, $methodName); + if ($method === null) { + throw new ShouldNotHappenException(); } - return $calledOnType->getMethod($methodName, $scope); + return $method; } } 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 new file mode 100644 index 0000000000..6edef9747b --- /dev/null +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -0,0 +1,174 @@ + + */ +final class MatchExpressionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $constantConditionRuleHelper, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return MatchExpressionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $matchCondition = $node->getCondition(); + $matchConditionType = $scope->getType($matchCondition); + $nextArmIsDeadForType = false; + $nextArmIsDeadForNativeType = false; + $errors = []; + $armsCount = count($node->getArms()); + $hasDefault = false; + foreach ($node->getArms() as $i => $arm) { + if ( + $nextArmIsDeadForNativeType + || ($nextArmIsDeadForType && $this->treatPhpDocTypesAsCertain) + ) { + continue; + } + $armConditions = $arm->getConditions(); + if (count($armConditions) === 0) { + $hasDefault = true; + } + foreach ($armConditions as $armCondition) { + $armConditionScope = $armCondition->getScope(); + $armConditionExpr = new Node\Expr\BinaryOp\Identical( + $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()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Match arm comparison between %s and %s is always false.', + $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), + $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), + ))->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 && !$nextArmIsDeadForType) { + $remainingType = $node->getEndScope()->getType($matchCondition); + $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()), + ))->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 de0ebaf218..a35a1c740e 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -2,25 +2,38 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function get_class; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp> + * @implements Rule */ -class NumberComparisonOperatorsConstantConditionRule implements \PHPStan\Rules\Rule +final class NumberComparisonOperatorsConstantConditionRule implements Rule { + public function __construct( + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + public function getNodeType(): string { return BinaryOp::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { if ( @@ -32,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(), + $exprType->getValue() ? 'true' : 'false', + )))->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 e09838544a..9ba6ee4797 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,22 +3,32 @@ 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 \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp> + * @implements Rule */ -class StrictComparisonOfDifferentTypesRule implements \PHPStan\Rules\Rule +final class StrictComparisonOfDifferentTypesRule implements Rule { - private bool $checkAlwaysTrueStrictComparison; - - public function __construct(bool $checkAlwaysTrueStrictComparison) + public function __construct( + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertainTip, + ) { - $this->checkAlwaysTrueStrictComparison = $checkAlwaysTrueStrictComparison; } public function getNodeType(): string @@ -28,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 1760d61625..10a359ea4b 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -2,36 +2,35 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Ternary> + * @implements Rule */ -class TernaryOperatorConstantConditionRule implements \PHPStan\Rules\Rule +final class TernaryOperatorConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\Ternary::class; + return Node\Expr\Ternary::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -45,14 +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' - )))->build(), + $exprType->getValue() ? 'true' : 'false', + )))->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 7dcf2be1c0..0000000000 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ -class UnreachableIfBranchesRule implements \PHPStan\Rules\Rule -{ - - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain - ) - { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $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()))->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())->build(); - } - - return $errors; - } - -} diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php deleted file mode 100644 index a7de019d6a..0000000000 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -class UnreachableTernaryElseBranchRule implements Rule -{ - - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain - ) - { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $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())->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php new file mode 100644 index 0000000000..ac87e2b8a1 --- /dev/null +++ b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php @@ -0,0 +1,33 @@ + + */ +final class UsageOfVoidMatchExpressionRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\Match_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + 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 new file mode 100644 index 0000000000..2b9fbdbdac --- /dev/null +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -0,0 +1,64 @@ + + */ +final class WhileLoopAlwaysFalseConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return While_::class; + } + + public function processNode( + Node $node, + Scope $scope, + ): array + { + $exprType = $this->helper->getBooleanType($scope, $node->cond); + if ($exprType->isFalse()->yes()) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->cond); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + return [ + $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) + ->identifier('while.alwaysFalse') + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php new file mode 100644 index 0000000000..8f6a1e3cf0 --- /dev/null +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -0,0 +1,91 @@ + + */ +final class WhileLoopAlwaysTrueConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return BreaklessWhileLoopNode::class; + } + + public function processNode( + Node $node, + Scope $scope, + ): array + { + foreach ($node->getExitPoints() as $exitPoint) { + $statement = $exitPoint->getStatement(); + if ($statement instanceof Break_) { + return []; + } + if (!$statement instanceof Continue_) { + return []; + } + if ($statement->num === null) { + continue; + } + if (!$statement->num instanceof Int_) { + continue; + } + $value = $statement->num->value; + if ($value === 1) { + continue; + } + + if ($value > 1) { + return []; + } + } + $originalNode = $node->getOriginalNode(); + $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); + if ($exprType->isTrue()->yes()) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $originalNode->cond); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + if (!$this->treatPhpDocTypesAsCertainTip) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + return [ + $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) + ->identifier('while.alwaysTrue') + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php new file mode 100644 index 0000000000..977bac234b --- /dev/null +++ b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php @@ -0,0 +1,30 @@ + + */ +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 d380317484..2bae50132b 100644 --- a/src/Rules/Constants/ConstantRule.php +++ b/src/Rules/Constants/ConstantRule.php @@ -4,14 +4,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ConstFetch> + * @implements Rule */ -class ConstantRule implements \PHPStan\Rules\Rule +final class ConstantRule implements Rule { + public function __construct( + private bool $discoveringSymbolsTip, + ) + { + } + public function getNodeType(): string { return Node\Expr\ConstFetch::class; @@ -20,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 - ))->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 new file mode 100644 index 0000000000..58b13d04e3 --- /dev/null +++ b/src/Rules/Constants/FinalConstantRule.php @@ -0,0 +1,43 @@ + */ +final class FinalConstantRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isFinal()) { + return []; + } + + if ($this->phpVersion->supportsFinalConstants()) { + return []; + } + + return [ + RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.') + ->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 new file mode 100644 index 0000000000..e91391e8bb --- /dev/null +++ b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php @@ -0,0 +1,26 @@ +extensions === null) { + $this->extensions = $this->container->getServicesByTag(AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG); + } + + return $this->extensions; + } + +} 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 new file mode 100644 index 0000000000..e0cbaa844c --- /dev/null +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -0,0 +1,96 @@ + + */ +final class MissingClassConstantTypehintRule implements Rule +{ + + public function __construct(private MissingTypehintCheck $missingTypehintCheck) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + { + $constantReflection = $classReflection->getConstant($constantName); + $constantType = $constantReflection->getPhpDocType(); + if ($constantType === null) { + return []; + } + + $errors = []; + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($constantType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s type has no value type specified in iterable type %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($constantType) as [$name, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s with generic %s does not specify its types: %s', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($constantType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s type has no signature specified for %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->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 new file mode 100644 index 0000000000..2b3fa162d1 --- /dev/null +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -0,0 +1,172 @@ + + */ +final class OverridingConstantRule implements Rule +{ + + public function __construct( + private bool $checkPhpDocMethodSignatures, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + { + $prototype = $this->findPrototype($classReflection, $constantName); + if ($prototype === null) { + return []; + } + + $constantReflection = $classReflection->getConstant($constantName); + $errors = []; + if ($prototype->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s overrides final constant %s::%s.', + $classReflection->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->identifier('classConstant.final')->nonIgnorable()->build(); + } + + if ($prototype->isPublic()) { + if (!$constantReflection->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s constant %s::%s overriding public constant %s::%s should also be public.', + $constantReflection->isPrivate() ? 'Private' : 'Protected', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->identifier('classConstant.visibility')->nonIgnorable()->build(); + } + } elseif ($constantReflection->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s overriding protected constant %s::%s should be protected or public.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->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; + } + + if (!$constantReflection->hasPhpDocType()) { + return $errors; + } + + if (!$prototype->getValueType()->isSuperTypeOf($constantReflection->getValueType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of constant %s::%s is not covariant with type %s of constant %s::%s.', + $constantReflection->getValueType()->describe(VerbosityLevel::value()), + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getValueType()->describe(VerbosityLevel::value()), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->identifier('classConstant.type')->build(); + } + + return $errors; + } + + private function findPrototype(ClassReflection $classReflection, string $constantName): ?ClassConstantReflection + { + foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasConstant($constantName)) { + return $immediateInterface->getConstant($constantName); + } + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return null; + } + + if (!$parentClass->hasConstant($constantName)) { + return null; + } + + $constant = $parentClass->getConstant($constantName); + if ($constant->isPrivate()) { + return null; + } + + return $constant; + } + +} diff --git a/src/Rules/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 new file mode 100644 index 0000000000..3e62e4b68b --- /dev/null +++ b/src/Rules/DateTimeInstantiationRule.php @@ -0,0 +1,71 @@ + + */ +final class DateTimeInstantiationRule implements Rule +{ + + public function getNodeType(): string + { + return New_::class; + } + + /** + * @param New_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof Node\Name) { + return []; + } + + $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 = []; + + 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', + $lowerClassName === 'datetime' ? 'DateTime' : 'DateTimeImmutable', + $dateString, + $error, + ))->identifier(sprintf('new.%s', $lowerClassName === 'datetime' ? 'dateTime' : 'dateTimeImmutable'))->build(); + } + } + + return $errors; + } + +} 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 69c40e4443..abc5200a71 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -3,60 +3,135 @@ 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 \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class NoopRule implements Rule +final class NoopRule implements Rule { - private Standard $printer; - - public function __construct(Standard $printer) + public function __construct(private ExprPrinter $exprPrinter) { - $this->printer = $printer; } 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\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())->build(), + $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 25819da0bf..17f166b788 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -9,9 +9,9 @@ use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\UnreachableStatementNode> + * @implements Rule */ -class UnreachableStatementRule implements Rule +final class UnreachableStatementRule implements Rule { public function getNodeType(): string @@ -21,12 +21,10 @@ 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.')->build(), + RuleErrorBuilder::message('Unreachable statement - code above always terminates.') + ->identifier('deadCode.unreachable') + ->build(), ]; } diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php new file mode 100644 index 0000000000..2e860a391f --- /dev/null +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -0,0 +1,106 @@ + + */ +final class UnusedPrivateConstantRule implements Rule +{ + + public function __construct(private AlwaysUsedClassConstantsExtensionProvider $extensionProvider) + { + } + + public function getNodeType(): string + { + return ClassConstantsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { + return []; + } + + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + + $constants = []; + foreach ($node->getConstants() as $constant) { + if (!$constant->isPrivate()) { + continue; + } + + foreach ($constant->consts as $const) { + $constantName = $const->name->toString(); + + $constantReflection = $classReflection->getConstant($constantName); + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysUsed($constantReflection)) { + continue 2; + } + } + + $constants[$constantName] = $const; + } + } + + 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; + } + + $constantReflection = $fetchScope->getConstantReflection($fetchedOnClass, $fetchNode->name->toString()); + if ($constantReflection === null) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); + } + continue; + } + + if ($constantReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); + } + continue; + } + + unset($constants[$fetchNode->name->toString()]); + } + + $errors = []; + foreach ($constants as $constantName => $constantNode) { + $errors[] = RuleErrorBuilder::message(sprintf('Constant %s::%s is unused.', $classReflection->getDisplayName(), $constantName)) + ->line($constantNode->getStartLine()) + ->identifier('classConstant.unused') + ->tip(sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-used-class-constants')) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php new file mode 100644 index 0000000000..e5b561af65 --- /dev/null +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -0,0 +1,195 @@ + + */ +final class UnusedPrivateMethodRule implements Rule +{ + + public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) + { + } + + public function getNodeType(): string + { + return ClassMethodsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { + return []; + } + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $constructor = null; + if ($classReflection->hasConstructor()) { + $constructor = $classReflection->getConstructor(); + } + + $methods = []; + foreach ($node->getMethods() as $method) { + if (!$method->getNode()->isPrivate()) { + continue; + } + if ($method->isDeclaredInTrait()) { + continue; + } + $methodName = $method->getNode()->name->toString(); + if ($constructor !== null && $constructor->getName() === $methodName) { + continue; + } + if (strtolower($methodName) === '__clone') { + continue; + } + + $methodReflection = $classReflection->getNativeMethod($methodName); + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysUsed($methodReflection)) { + continue 2; + } + } + + $methods[strtolower($methodName)] = $method; + } + + $arrayCalls = []; + foreach ($node->getMethodCalls() as $methodCall) { + $methodCallNode = $methodCall->getNode(); + if ($methodCallNode instanceof Node\Expr\Array_) { + $arrayCalls[] = $methodCall; + continue; + } + $callScope = $methodCall->getScope(); + if ($methodCallNode->name instanceof Identifier) { + $methodNames = [$methodCallNode->name->toString()]; + } else { + $methodNameType = $callScope->getType($methodCallNode->name); + $strings = $methodNameType->getConstantStrings(); + if (count($strings) === 0) { + // 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); + } + + if ($methodCallNode instanceof Node\Expr\MethodCall) { + $calledOnType = $callScope->getType($methodCallNode->var); + } else { + if ($methodCallNode->class instanceof Node\Name) { + $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); + } else { + $calledOnType = $callScope->getType($methodCallNode->class); + } + } + + $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[strtolower($methodName)]); + } + } + + if (count($methods) > 0) { + foreach ($arrayCalls as $arrayCall) { + /** @var Node\Expr\Array_ $array */ + $array = $arrayCall->getNode(); + $arrayScope = $arrayCall->getScope(); + $arrayType = $arrayScope->getType($array); + if (!$arrayType->isCallable()->yes()) { + 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())]); + } + } + } + } + + $errors = []; + foreach ($methods as $method) { + $originalMethodName = $method->getNode()->name->toString(); + $methodType = 'Method'; + if ($method->getNode()->isStatic()) { + $methodType = 'Static method'; + } + $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $originalMethodName)) + ->line($method->getNode()->getStartLine()) + ->identifier('method.unused') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php new file mode 100644 index 0000000000..239e732056 --- /dev/null +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -0,0 +1,260 @@ + + */ +final class UnusedPrivatePropertyRule implements Rule +{ + + /** + * @param string[] $alwaysWrittenTags + * @param string[] $alwaysReadTags + */ + public function __construct( + private ReadWritePropertiesExtensionProvider $extensionProvider, + private array $alwaysWrittenTags, + private array $alwaysReadTags, + private bool $checkUninitializedProperties, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertiesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getClass() instanceof Node\Stmt\Class_) { + return []; + } + $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 = !$property->isReadable(); + $alwaysWritten = !$property->isWritable(); + if ($property->getPhpDoc() !== null) { + $text = $property->getPhpDoc(); + foreach ($this->alwaysReadTags as $tag) { + if (!str_contains($text, $tag)) { + continue; + } + + $alwaysRead = true; + break; + } + + foreach ($this->alwaysWrittenTags as $tag) { + if (!str_contains($text, $tag)) { + continue; + } + + $alwaysWritten = true; + break; + } + } + + $propertyName = $property->getName(); + if (!$alwaysRead || !$alwaysWritten) { + if (!$classReflection->hasNativeProperty($propertyName)) { + continue; + } + + $propertyReflection = $classReflection->getNativeProperty($propertyName); + + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($alwaysRead && $alwaysWritten) { + break; + } + if (!$alwaysRead && $extension->isAlwaysRead($propertyReflection, $propertyName)) { + $alwaysRead = true; + } + if ($alwaysWritten || !$extension->isAlwaysWritten($propertyReflection, $propertyName)) { + continue; + } + + $alwaysWritten = true; + } + } + + $read = $alwaysRead; + $written = $alwaysWritten || $property->getDefault() !== null; + $properties[$propertyName] = [ + '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) { + $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 = $usageScope->getType($fetch->name); + $strings = $propertyNameType->getConstantStrings(); + if (count($strings) === 0) { + // 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 = $usageScope->getType($fetch->var); + } else { + if ($fetch->class instanceof Node\Name) { + $fetchedOnType = $usageScope->resolveTypeByName($fetch->class); + } else { + $fetchedOnType = $usageScope->getType($fetch->class); + } + } + + 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 { + $properties[$propertyName]['written'] = true; + } + } + } + + [$uninitializedProperties] = $node->getUninitializedProperties($scope, []); + + $errors = []; + foreach ($properties as $name => $data) { + $propertyNode = $data['node']; + if ($propertyNode->isStatic()) { + $propertyName = sprintf('Static property %s::$%s', $classReflection->getDisplayName(), $name); + } else { + $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()) + ->tip($tip) + ->identifier('property.unused') + ->build(); + } else { + 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)) { + 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(); + } + } + } + + return $errors; + } + +} 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 new file mode 100644 index 0000000000..f7d2a6dcb4 --- /dev/null +++ b/src/Rules/Debug/DumpTypeRule.php @@ -0,0 +1,59 @@ + + */ +final class DumpTypeRule 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\dumptype') { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Dumped type: %s', + $scope->getType($node->getArgs()[0]->value)->describe(VerbosityLevel::precise()), + ), + )->nonIgnorable()->identifier('phpstan.dumpType')->build(), + ]; + } + +} diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php new file mode 100644 index 0000000000..769f37bd1c --- /dev/null +++ b/src/Rules/Debug/FileAssertRule.php @@ -0,0 +1,202 @@ + + */ +final class FileAssertRule 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 []; + } + + $function = $this->reflectionProvider->getFunction($node->name, $scope); + if ($function->getName() === 'PHPStan\\Testing\\assertType') { + return $this->processAssertType($node->getArgs(), $scope); + } + + if ($function->getName() === 'PHPStan\\Testing\\assertNativeType') { + return $this->processAssertNativeType($node->getArgs(), $scope); + } + + if ($function->getName() === 'PHPStan\\Testing\\assertVariableCertainty') { + return $this->processAssertVariableCertainty($node->getArgs(), $scope); + } + + return []; + } + + /** + * @param Node\Arg[] $args + * @return list + */ + private function processAssertType(array $args, Scope $scope): array + { + if (count($args) !== 2) { + return []; + } + + $expectedTypeStrings = $scope->getType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { + return [ + RuleErrorBuilder::message('Expected type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + + $expressionType = $scope->getType($args[1]->value)->describe(VerbosityLevel::precise()); + if ($expectedTypeStrings[0]->getValue() === $expressionType) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.type') + ->build(), + ]; + } + + /** + * @param Node\Arg[] $args + * @return list + */ + private function processAssertNativeType(array $args, Scope $scope): array + { + if (count($args) !== 2) { + return []; + } + + $expectedTypeStrings = $scope->getNativeType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { + return [ + 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 ($expectedTypeStrings[0]->getValue() === $expressionType) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.nativeType') + ->build(), + ]; + } + + /** + * @param Node\Arg[] $args + * @return list + */ + private function processAssertVariableCertainty(array $args, Scope $scope): array + { + if (count($args) !== 2) { + return []; + } + + $certainty = $args[0]->value; + if (!$certainty instanceof StaticCall) { + return [ + RuleErrorBuilder::message('First argument of %s() must be TrinaryLogic call') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + if (!$certainty->class instanceof Node\Name) { + return [ + RuleErrorBuilder::message('Invalid TrinaryLogic call.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + + if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') { + return [ + RuleErrorBuilder::message('Invalid TrinaryLogic call.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + + if (!$certainty->name instanceof Node\Identifier) { + return [ + RuleErrorBuilder::message('Invalid TrinaryLogic call.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), + ]; + } + + // @phpstan-ignore staticMethod.dynamicName + $expectedCertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); + $variable = $args[1]->value; + 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(), + ]; + } + + if ($expectedCertaintyValue->equals($actualCertaintyValue)) { + return []; + } + + return [ + 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 new file mode 100644 index 0000000000..c7e0113764 --- /dev/null +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -0,0 +1,36 @@ + + */ +final class EnumCaseAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return Node\Stmt\EnumCase::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->attrGroups, + Attribute::TARGET_CLASS_CONSTANT, + 'class constant', + ); + } + +} diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php new file mode 100644 index 0000000000..d7d3b5bc71 --- /dev/null +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -0,0 +1,69 @@ + + */ +final class CatchWithUnthrownExceptionRule implements Rule +{ + + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $reportUncheckedExceptionDeadCatch, + ) + { + } + + public function getNodeType(): string + { + return CatchWithUnthrownExceptionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->getCaughtType() instanceof NeverType) { + return [ + RuleErrorBuilder::message( + sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())), + ) + ->line($node->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->getStartLine()) + ->identifier('catch.neverThrown') + ->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index 77b94c352f..d873394c20 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -6,31 +6,28 @@ 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; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Catch_> + * @implements Rule */ -class CaughtExceptionExistenceRule implements \PHPStan\Rules\Rule +final class CaughtExceptionExistenceRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private bool $checkClassCaseSensitivity, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string @@ -47,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())->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->getNativeReflection()->implementsInterface(\Throwable::class)) { - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName()))->line($class->getLine())->build(); - } - - if (!$this->checkClassCaseSensitivity) { - continue; + if (!$classReflection->isInterface() && !$classReflection->implementsInterface(Throwable::class)) { + $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/DeadCatchRule.php b/src/Rules/Exceptions/DeadCatchRule.php deleted file mode 100644 index 88438b0689..0000000000 --- a/src/Rules/Exceptions/DeadCatchRule.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ -class DeadCatchRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Stmt\TryCatch::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $catchTypes = array_map(static function (Node\Stmt\Catch_ $catch): Type { - return TypeCombinator::union(...array_map(static function (Node\Name $className): ObjectType { - return new ObjectType($className->toString()); - }, $catch->types)); - }, $node->catches); - $catchesCount = count($catchTypes); - $errors = []; - for ($i = 0; $i < $catchesCount - 1; $i++) { - $firstType = $catchTypes[$i]; - for ($j = $i + 1; $j < $catchesCount; $j++) { - $secondType = $catchTypes[$j]; - if (!$firstType->isSuperTypeOf($secondType)->yes()) { - continue; - } - - $errors[] = RuleErrorBuilder::message(sprintf( - 'Dead catch - %s is already caught by %s above.', - $secondType->describe(VerbosityLevel::typeOnly()), - $firstType->describe(VerbosityLevel::typeOnly()) - ))->line($node->catches[$j]->getLine())->build(); - } - } - - return $errors; - } - -} diff --git a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php new file mode 100644 index 0000000000..f428436b48 --- /dev/null +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -0,0 +1,92 @@ +uncheckedExceptionRegexes as $regex) { + if (Strings::match($className, $regex) !== null) { + return false; + } + } + + foreach ($this->uncheckedExceptionClasses as $uncheckedExceptionClass) { + if ($className === $uncheckedExceptionClass) { + return false; + } + } + + if (!$this->reflectionProvider->hasClass($className)) { + return $this->isCheckedExceptionInternal($className); + } + + $classReflection = $this->reflectionProvider->getClass($className); + foreach ($this->uncheckedExceptionClasses as $uncheckedExceptionClass) { + if (!$classReflection->is($uncheckedExceptionClass)) { + continue; + } + + return false; + } + + return $this->isCheckedExceptionInternal($className); + } + + private function isCheckedExceptionInternal(string $className): bool + { + foreach ($this->checkedExceptionRegexes as $regex) { + if (Strings::match($className, $regex) !== null) { + return true; + } + } + + foreach ($this->checkedExceptionClasses as $checkedExceptionClass) { + if ($className === $checkedExceptionClass) { + return true; + } + } + + if (!$this->reflectionProvider->hasClass($className)) { + return count($this->checkedExceptionRegexes) === 0 && count($this->checkedExceptionClasses) === 0; + } + + $classReflection = $this->reflectionProvider->getClass($className); + foreach ($this->checkedExceptionClasses as $checkedExceptionClass) { + if (!$classReflection->is($checkedExceptionClass)) { + continue; + } + + return true; + } + + return count($this->checkedExceptionRegexes) === 0 && count($this->checkedExceptionClasses) === 0; + } + +} diff --git a/src/Rules/Exceptions/ExceptionTypeResolver.php b/src/Rules/Exceptions/ExceptionTypeResolver.php new file mode 100644 index 0000000000..5b7ac7e965 --- /dev/null +++ b/src/Rules/Exceptions/ExceptionTypeResolver.php @@ -0,0 +1,39 @@ + + */ +final class MissingCheckedExceptionInFunctionThrowsRule implements Rule +{ + + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $functionReflection = $node->getFunctionReflection(); + + $errors = []; + 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->getStartLine()) + ->identifier('missingType.checkedException') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php new file mode 100644 index 0000000000..c564711b2f --- /dev/null +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -0,0 +1,48 @@ + + */ +final class MissingCheckedExceptionInMethodThrowsRule implements Rule +{ + + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $methodReflection = $node->getMethodReflection(); + + $errors = []; + 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->getStartLine()) + ->identifier('missingType.checkedException') + ->build(); + } + + return $errors; + } + +} 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 new file mode 100644 index 0000000000..0756fcac6b --- /dev/null +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -0,0 +1,61 @@ + + */ + public function check(?Type $throwType, array $throwPoints): array + { + if ($throwType === null) { + $throwType = new NeverType(); + } + + $classes = []; + foreach ($throwPoints as $throwPoint) { + if (!$throwPoint->isExplicit()) { + continue; + } + + foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { + if ($throwPointType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + continue; + } + if ($throwType->isSuperTypeOf($throwPointType)->yes()) { + continue; + } + + $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()]; + } + } + + return $classes; + } + +} 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 new file mode 100644 index 0000000000..74fa2eb0ae --- /dev/null +++ b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php @@ -0,0 +1,69 @@ + + */ +final class OverwrittenExitPointByFinallyRule implements Rule +{ + + public function getNodeType(): string + { + return FinallyExitPointsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getTryCatchExitPoints()) === 0) { + return []; + } + + $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()->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()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); + } + + return $errors; + } + + private function describeExitPoint(Node\Stmt $stmt): string + { + if ($stmt instanceof Node\Stmt\Return_) { + return 'return'; + } + + if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Node\Expr\Throw_) { + return 'throw'; + } + + if ($stmt instanceof Node\Stmt\Continue_) { + return 'continue'; + } + + if ($stmt instanceof Node\Stmt\Break_) { + return 'break'; + } + + return 'exit point'; + } + +} 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 new file mode 100644 index 0000000000..9fcc9c9e88 --- /dev/null +++ b/src/Rules/Exceptions/ThrowExpressionRule.php @@ -0,0 +1,44 @@ + + */ +final class ThrowExpressionRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Expr\Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->phpVersion->supportsThrowExpression()) { + return []; + } + + if ($node->getAttribute(StandaloneThrowExprVisitor::ATTRIBUTE_NAME) === true) { + return []; + } + + return [ + 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 new file mode 100644 index 0000000000..a2ead680c7 --- /dev/null +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php @@ -0,0 +1,71 @@ + + */ +final class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule +{ + + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $missingCheckedExceptionInThrows, + ) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $functionReflection = $node->getFunctionReflection(); + + if ($functionReflection->getThrowType() === null || !$functionReflection->getThrowType()->isVoid()->yes()) { + return []; + } + + $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( + 'Function %s() throws exception %s but the PHPDoc contains @throws void.', + $functionReflection->getName(), + $throwPointType->describe(VerbosityLevel::typeOnly()), + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php new file mode 100644 index 0000000000..327b55c202 --- /dev/null +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php @@ -0,0 +1,72 @@ + + */ +final class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule +{ + + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $missingCheckedExceptionInThrows, + ) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $methodReflection = $node->getMethodReflection(); + + if ($methodReflection->getThrowType() === null || !$methodReflection->getThrowType()->isVoid()->yes()) { + return []; + } + + $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( + 'Method %s::%s() throws exception %s but the PHPDoc contains @throws void.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $throwPointType->describe(VerbosityLevel::typeOnly()), + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); + } + } + + return $errors; + } + +} 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 new file mode 100644 index 0000000000..6688dec466 --- /dev/null +++ b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php @@ -0,0 +1,51 @@ + + */ +final class TooWideFunctionThrowTypeRule implements Rule +{ + + public function __construct(private TooWideThrowTypeCheck $check) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $functionReflection = $node->getFunctionReflection(); + + $throwType = $functionReflection->getThrowType(); + if ($throwType === null) { + return []; + } + + $errors = []; + foreach ($this->check->check($throwType, $statementResult->getThrowPoints()) as $throwClass) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Function %s() has %s in PHPDoc @throws tag but it\'s not thrown.', + $functionReflection->getName(), + $throwClass, + )) + ->identifier('throws.unusedType') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php new file mode 100644 index 0000000000..55c69ee3a5 --- /dev/null +++ b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php @@ -0,0 +1,67 @@ + + */ +final class TooWideMethodThrowTypeRule implements Rule +{ + + public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $statementResult = $node->getStatementResult(); + $methodReflection = $node->getMethodReflection(); + $classReflection = $node->getClassReflection(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $methodReflection->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( + 'Method %s::%s() has %s in PHPDoc @throws tag but it\'s not thrown.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $throwClass, + )) + ->identifier('throws.unusedType') + ->build(); + } + + return $errors; + } + +} 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 new file mode 100644 index 0000000000..5c5e33c803 --- /dev/null +++ b/src/Rules/Exceptions/TooWideThrowTypeCheck.php @@ -0,0 +1,50 @@ +isVoid()->yes()) { + return []; + } + + $throwPointType = TypeCombinator::union(...array_map(function (ThrowPoint $throwPoint): Type { + if (!$this->implicitThrows && !$throwPoint->isExplicit()) { + return new NeverType(); + } + + return $throwPoint->getType(); + }, $throwPoints)); + + $throwClasses = []; + foreach (TypeUtils::flattenTypes($throwType) as $type) { + if (!$throwPointType instanceof NeverType && !$type->isSuperTypeOf($throwPointType)->no()) { + continue; + } + + $throwClasses[] = $type->describe(VerbosityLevel::typeOnly()); + } + + return $throwClasses; + } + +} 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 00cdd24af5..61702c6194 100644 --- a/src/Rules/FoundTypeResult.php +++ b/src/Rules/FoundTypeResult.php @@ -4,31 +4,23 @@ use PHPStan\Type\Type; -class FoundTypeResult +/** + * @api + */ +final class FoundTypeResult { - private \PHPStan\Type\Type $type; - - /** @var string[] */ - private array $referencedClasses; - - /** @var RuleError[] */ - private array $unknownClassErrors; - /** - * @param \PHPStan\Type\Type $type * @param string[] $referencedClasses - * @param RuleError[] $unknownClassErrors + * @param list $unknownClassErrors */ public function __construct( - Type $type, - array $referencedClasses, - array $unknownClassErrors + private Type $type, + private array $referencedClasses, + private array $unknownClassErrors, + private ?string $tip, ) { - $this->type = $type; - $this->referencedClasses = $referencedClasses; - $this->unknownClassErrors = $unknownClassErrors; } public function getType(): Type @@ -45,11 +37,16 @@ public function getReferencedClasses(): array } /** - * @return RuleError[] + * @return list */ public function getUnknownClassErrors(): array { return $this->unknownClassErrors; } + public function getTip(): ?string + { + return $this->tip; + } + } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index e3d6320d7a..aed8352077 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -2,54 +2,81 @@ namespace PHPStan\Rules; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +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 { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $checkArgumentTypes; - - private bool $checkArgumentsPassedByReference; - - private bool $checkExtraArguments; - - private bool $checkMissingTypehints; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - bool $checkArgumentTypes, - bool $checkArgumentsPassedByReference, - bool $checkExtraArguments, - bool $checkMissingTypehints + private RuleLevelHelper $ruleLevelHelper, + private NullsafeCheck $nullsafeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkArgumentTypes, + private bool $checkArgumentsPassedByReference, + private bool $checkExtraArguments, + private bool $checkMissingTypehints, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->checkArgumentTypes = $checkArgumentTypes; - $this->checkArgumentsPassedByReference = $checkArgumentsPassedByReference; - $this->checkExtraArguments = $checkExtraArguments; - $this->checkMissingTypehints = $checkMissingTypehints; } /** - * @param \PHPStan\Reflection\ParametersAcceptor $parametersAcceptor - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\New_ $funcCall - * @param string[] $messages Nine message templates - * @return RuleError[] + * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType + * @return list */ public function check( ParametersAcceptor $parametersAcceptor, Scope $scope, - $funcCall, - array $messages + bool $isBuiltin, + 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; @@ -66,66 +93,195 @@ public function check( $functionParametersMaxCount = -1; } + /** @var array $arguments */ + $arguments = []; + /** @var array $args */ + $args = $funcCall->getArgs(); + $hasNamedArguments = false; + $hasUnpackedArgument = false; $errors = []; - $invokedParametersCount = count($funcCall->args); - foreach ($funcCall->args as $arg) { + 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.') + ->identifier('argument.unpackAfterNamed') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); + } + if ($hasUnpackedArgument && !$arg->unpack) { + 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) { - $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); - break; + $hasUnpackedArgument = true; } + if ($arg->unpack) { + $type = $scope->getType($arg->value); + $arrays = $type->getConstantArrays(); + if (count($arrays) > 0) { + $maxKeys = null; + foreach ($arrays as $array) { + $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; + } + + $maxKeys = $keysCount; + } + + 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) { + $commonKey = $keyType->getValue(); + } elseif ($commonKey !== $keyType->getValue()) { + $commonKey = false; + } + } + $keyArgumentName = null; + if (is_string($commonKey)) { + $keyArgumentName = $commonKey; + $hasNamedArguments = true; + } + if ($isOptionalKey) { + continue; + } + + $arguments[] = [ + $arg->value, + TypeCombinator::union(...$types), + false, + $keyArgumentName, + $arg->getStartLine(), + ]; + } + } else { + $arguments[] = [ + $arg->value, + $type->getIterableValueType(), + true, + null, + $arg->getStartLine(), + ]; + } + continue; + } + + $arguments[] = [ + $arg->value, + null, + false, + $argumentName, + $arg->getStartLine(), + ]; } - if ( - $invokedParametersCount < $functionParametersMinCount - || ($this->checkExtraArguments && $invokedParametersCount > $functionParametersMaxCount) - ) { - if ($functionParametersMinCount === $functionParametersMaxCount) { - $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[0] : $messages[1], - $invokedParametersCount, - $functionParametersMinCount - ))->build(); - } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { - $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[2] : $messages[3], - $invokedParametersCount, - $functionParametersMinCount - ))->build(); - } elseif ($functionParametersMaxCount !== -1) { - $errors[] = RuleErrorBuilder::message(sprintf( - $invokedParametersCount === 1 ? $messages[4] : $messages[5], - $invokedParametersCount, - $functionParametersMinCount, - $functionParametersMaxCount - ))->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 [$argumentValue, $argumentValueType, $unpack, $argumentName]) { + if ($unpack) { + $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); + break; + } + } + + if ( + $invokedParametersCount < $functionParametersMinCount + || ($this->checkExtraArguments && $invokedParametersCount > $functionParametersMaxCount) + ) { + if ($functionParametersMinCount === $functionParametersMaxCount) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invokedParametersCount === 1 ? $singleInsufficientParameterMessage : $pluralInsufficientParametersMessage, + $invokedParametersCount, + $functionParametersMinCount, + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); + } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invokedParametersCount === 1 ? $singleInsufficientParameterInVariadicFunctionMessage : $pluralInsufficientParametersInVariadicFunctionMessage, + $invokedParametersCount, + $functionParametersMinCount, + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); + } elseif ($functionParametersMaxCount !== -1) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invokedParametersCount === 1 ? $singleInsufficientParameterWithOptionalParametersMessage : $pluralInsufficientParametersWithOptionalParametersMessage, + $invokedParametersCount, + $functionParametersMinCount, + $functionParametersMaxCount, + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); + } } } if ( - $scope->getType($funcCall) instanceof VoidType + !$funcCall instanceof Node\Expr\New_ && !$scope->isInFirstLevelStatement() - && !$funcCall instanceof \PhpParser\Node\Expr\New_ + && $scope->getKeepVoidType($funcCall)->isVoid()->yes() ) { - $errors[] = RuleErrorBuilder::message($messages[7])->build(); + $errors[] = RuleErrorBuilder::message($voidReturnTypeUsed) + ->identifier(sprintf('%s.void', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); + } + + [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getStartLine(), $isBuiltin, $arguments, $hasNamedArguments, $missingParameterMessage, $unknownParameterMessage); + foreach ($addedErrors as $error) { + $errors[] = $error; } if (!$this->checkArgumentTypes && !$this->checkArgumentsPassedByReference) { return $errors; } - $parameters = $parametersAcceptor->getParameters(); - - /** @var array $args */ - $args = $funcCall->args; - foreach ($args as $i => $argument) { - if ($this->checkArgumentTypes && $argument->unpack) { + foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]) { + if ($this->checkArgumentTypes && $unpack) { $iterableTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $argument->value, + $argumentValue, '', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $iterableTypeResultType = $iterableTypeResult->getType(); if ( @@ -135,74 +291,377 @@ static function (Type $type): bool { $errors[] = RuleErrorBuilder::message(sprintf( 'Only iterables can be unpacked, %s given in argument #%d.', $iterableTypeResultType->describe(VerbosityLevel::typeOnly()), - $i + 1 - ))->build(); + $i + 1, + ))->identifier('argument.unpackNonIterable')->line($argumentLine)->build(); } } - if (!isset($parameters[$i])) { - if (!$parametersAcceptor->isVariadic() || count($parameters) === 0) { - break; + if ($parameter === null) { + continue; + } + + if ($argumentValueType === null) { + if ($scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); } + $argumentValueType = $scope->getType($argumentValue); - $parameter = $parameters[count($parameters) - 1]; - if (!$parameter->isVariadic()) { - break; // func_get_args + 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(); + } } - } else { - $parameter = $parameters[$i]; } - $parameterType = $parameter->getType(); + 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(); + } + } - $argumentValueType = $scope->getType($argument->value); - if ($argument->unpack) { - $argumentValueType = $argumentValueType->getIterableValueType(); + 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 ( - $this->checkArgumentTypes - && !$parameter->passedByReference()->createsNewVariable() - && !$this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()) + !$this->checkArgumentsPassedByReference + || !$parameter->passedByReference()->yes() ) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType); + continue; + } + + if ($this->nullsafeCheck->containsNullSafe($argumentValue)) { $errors[] = RuleErrorBuilder::message(sprintf( - $messages[6], - $i + 1, - sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()), - $parameterType->describe($verbosityLevel), - $argumentValueType->describe($verbosityLevel) - ))->build(); + $parameterPassedByReferenceMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + )) + ->identifier('argument.byRef') + ->line($argumentLine) + ->build(); + continue; } if ( - !$this->checkArgumentsPassedByReference - || !$parameter->passedByReference()->yes() - || $argument->value instanceof \PhpParser\Node\Expr\Variable - || $argument->value instanceof \PhpParser\Node\Expr\ArrayDimFetch - || $argument->value instanceof \PhpParser\Node\Expr\PropertyFetch - || $argument->value instanceof \PhpParser\Node\Expr\StaticPropertyFetch - ) { + $argumentValue instanceof Node\Expr\PropertyFetch + || $argumentValue instanceof Node\Expr\StaticPropertyFetch) { + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($argumentValue, $scope); + foreach ($propertyReflections as $propertyReflection) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null) { + continue; + } + + if ($nativePropertyReflection->isReadOnly()) { + 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 { + continue; + } + + $propertyDescription = sprintf($errorFormat, $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is passed by reference so it does not accept %s.', + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + $propertyDescription, + ))->identifier('argument.byRef')->line($argumentLine)->build(); + } + } + + if ($argumentValue instanceof Node\Expr\Variable + || $argumentValue instanceof Node\Expr\ArrayDimFetch + || $argumentValue instanceof Node\Expr\PropertyFetch + || $argumentValue instanceof Node\Expr\StaticPropertyFetch) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( - $messages[8], - $i + 1, - sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()) - ))->build(); + $parameterPassedByReferenceMessage, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.byRef')->line($argumentLine)->build(); } - if ($this->checkMissingTypehints) { - foreach ($parametersAcceptor->getResolvedTemplateTypeMap()->getTypes() as $name => $type) { - if (!($type instanceof ErrorType) && !($type instanceof NeverType)) { - continue; + if ($this->checkMissingTypehints && $parametersAcceptor instanceof ResolvedFunctionVariant) { + $originalParametersAcceptor = $parametersAcceptor->getOriginalParametersAcceptor(); + $resolvedTypes = $parametersAcceptor->getResolvedTemplateTypeMap()->getTypes(); + if (count($resolvedTypes) > 0) { + $returnTemplateTypes = []; + TypeTraverser::map( + $parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(), + static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Type { + while ($type instanceof ConditionalType && $type->isResolvable()) { + $type = $type->resolve(); + } + + 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 && $type->getDefault() === null) { + $parameterTemplateTypes[$type->getName()] = true; + return $type; + } + + return $traverse($type); + }); } - $errors[] = RuleErrorBuilder::message(sprintf($messages[9], $name))->build(); + foreach ($resolvedTypes as $name => $type) { + if ( + !($type instanceof ErrorType) + && ( + !$type instanceof NeverType + || $type->isExplicit() + ) + ) { + continue; + } + + if (!array_key_exists($name, $returnTemplateTypes)) { + continue; + } + + if (!array_key_exists($name, $parameterTemplateTypes)) { + continue; + } + + $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(); + } + } + + if ( + !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) + && $this->unresolvableTypeHelper->containsUnresolvableType($parametersAcceptor->getReturnType()) + ) { + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->identifier(sprintf('%s.unresolvableReturnType', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } } return $errors; } + /** + * @param array $arguments + * @return array{list, array} + */ + private function processArguments( + ParametersAcceptor $parametersAcceptor, + int $line, + bool $isBuiltin, + array $arguments, + bool $hasNamedArguments, + string $missingParameterMessage, + string $unknownParameterMessage, + ): array + { + $parameters = $parametersAcceptor->getParameters(); + $originalParameters = $parametersAcceptor instanceof ResolvedFunctionVariant + ? $parametersAcceptor->getOriginalParametersAcceptor()->getParameters() + : array_fill(0, count($parameters), null); + $parametersByName = []; + $originalParametersByName = []; + $unusedParametersByName = []; + $errors = []; + foreach ($parameters as $i => $parameter) { + $parametersByName[$parameter->getName()] = $parameter; + $originalParametersByName[$parameter->getName()] = $originalParameters[$i]; + + if ($parameter->isVariadic()) { + continue; + } + + $unusedParametersByName[$parameter->getName()] = $parameter; + } + + $newArguments = []; + + $namedArgumentAlreadyOccurred = false; + foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine]) { + if ($argumentName === null) { + if (!isset($parameters[$i])) { + if (!$parametersAcceptor->isVariadic() || count($parameters) === 0) { + $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, 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; + + $parametersCount = count($parameters); + if ( + !$parametersAcceptor->isVariadic() + || $parametersCount <= 0 + || $isBuiltin + ) { + $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.') + ->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, $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())) + ->identifier('argument.duplicate') + ->line($argumentLine) + ->build(); + continue; + } + + unset($unusedParametersByName[$parameter->getName()]); + } + + if ($hasNamedArguments) { + foreach ($unusedParametersByName as $parameter) { + if ($parameter->isOptional()) { + continue; + } + + $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 3aa0c27bd3..af468ab670 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -2,175 +2,373 @@ 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\Reflection\FunctionReflection; +use PHPStan\Node\Printer\NodeTypePrinter; +use PHPStan\Php\PhpVersion; +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; - -class FunctionDefinitionCheck +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; + +final class FunctionDefinitionCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - - private bool $checkThisOnly; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity, - bool $checkThisOnly + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PhpVersion $phpVersion, + private bool $checkClassCaseSensitivity, + private bool $checkThisOnly, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkThisOnly = $checkThisOnly; } /** - * @param \PhpParser\Node\Stmt\Function_ $function - * @param string $parameterMessage - * @param string $returnMessage - * @return RuleError[] + * @return list */ public function checkFunction( + Scope $scope, Function_ $function, - FunctionReflection $functionReflection, + PhpFunctionFromParserNodeReflection $functionReflection, string $parameterMessage, - string $returnMessage + string $returnMessage, + string $unionTypesMessage, + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - return $this->checkParametersAcceptor( - $parametersAcceptor, + $scope, + $functionReflection, $function, $parameterMessage, - $returnMessage + $returnMessage, + $unionTypesMessage, + $templateTypeMissingInParameterMessage, + $unresolvableParameterTypeMessage, + $unresolvableReturnTypeMessage, ); } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Param[] $parameters - * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $returnTypeNode - * @param string $parameterMessage - * @param string $returnMessage - * @return \PHPStan\Rules\RuleError[] + * @param Node\Param[] $parameters + * @param Node\Identifier|Node\Name|Node\ComplexType|null $returnTypeNode + * @return list */ public function checkAnonymousFunction( Scope $scope, array $parameters, $returnTypeNode, string $parameterMessage, - string $returnMessage + string $returnMessage, + string $unionTypesMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { $errors = []; - foreach ($parameters as $param) { + $unionTypeReported = false; + foreach ($parameters as $i => $param) { if ($param->type === null) { continue; } + if ( + !$unionTypeReported + && $param->type instanceof UnionType + && !$this->phpVersion->supportsNativeUnionTypes() + ) { + $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 \PHPStan\ShouldNotHappenException(); + 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->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), + ); } } + if ($this->phpVersion->deprecatesRequiredParameterAfterOptional()) { + $errors = array_merge($errors, $this->checkRequiredParameterAfterOptional($parameters)); + } + if ($returnTypeNode === null) { return $errors; } + if ( + !$unionTypeReported + && $returnTypeNode instanceof UnionType + && !$this->phpVersion->supportsNativeUnionTypes() + ) { + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unionTypeNotSupported') + ->nonIgnorable() + ->build(); + } + $returnType = $scope->getFunctionType($returnTypeNode, false, false); + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($returnType) + ) { + $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; } /** - * @param PhpMethodFromParserNodeReflection $methodReflection - * @param ClassMethod $methodNode - * @param string $parameterMessage - * @param string $returnMessage - * @return RuleError[] + * @return list */ public function checkClassMethod( + Scope $scope, PhpMethodFromParserNodeReflection $methodReflection, - ClassMethod $methodNode, + ClassMethod|Node\PropertyHook $methodNode, string $parameterMessage, - string $returnMessage + string $returnMessage, + string $unionTypesMessage, + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, + string $selfOutMessage, ): array { - /** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs $parametersAcceptor */ - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - - return $this->checkParametersAcceptor( - $parametersAcceptor, + $errors = $this->checkParametersAcceptor( + $scope, + $methodReflection, $methodNode, $parameterMessage, - $returnMessage + $returnMessage, + $unionTypesMessage, + $templateTypeMissingInParameterMessage, + $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; } /** - * @param ParametersAcceptor $parametersAcceptor - * @param FunctionLike $functionNode - * @param string $parameterMessage - * @param string $returnMessage - * @return RuleError[] + * @return list */ private function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + Scope $scope, + PhpMethodFromParserNodeReflection|PhpFunctionFromParserNodeReflection $parametersAcceptor, FunctionLike $functionNode, string $parameterMessage, - string $returnMessage + string $returnMessage, + string $unionTypesMessage, + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { $errors = []; $parameterNodes = $functionNode->getParams(); + if (!$this->phpVersion->supportsNativeUnionTypes()) { + $unionTypeReported = false; + foreach ($parameterNodes as $parameterNode) { + if (!$parameterNode->type instanceof UnionType) { + continue; + } + + $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()->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()) { + $errors = array_merge($errors, $this->checkRequiredParameterAfterOptional($parameterNodes)); + } + $returnTypeNode = $functionNode->getReturnType() ?? $functionNode; foreach ($parametersAcceptor->getParameters() as $parameter) { $referencedClasses = $this->getParameterReferencedClasses($parameter); @@ -182,80 +380,272 @@ private function checkParametersAcceptor( return $parameterNode; }; + $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 ( - $parameter instanceof ParameterReflectionWithPhpDocs - && $parameter->getNativeType() instanceof VoidType + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) ) { - $parameterVar = $parameterNodeCallback()->var; - if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name)) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($referencedClasses as $class) { - if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf( + $parameterMessage, + $parameter->getName(), + $class, + )) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( $parameterMessage, $parameter->getName(), - $class - ))->line($parameterNodeCallback()->getLine())->build(); + $class, + )) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.trait') + ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static function (string $class) use ($parameterNodeCallback): ClassNameNodePair { - return new ClassNameNodePair($class, $parameterNodeCallback()); - }, $referencedClasses)) - ); + $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->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 function (string $class) use ($returnTypeNode): ClassNameNodePair { - return 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(); + $templateTypes = $templateTypeMap->getTypes(); + if (count($templateTypes) > 0) { + foreach ($parametersAcceptor->getParameters() as $parameter) { + TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$templateTypes): Type { + if ($type instanceof TemplateType) { + unset($templateTypes[$type->getName()]); + return $traverse($type); + } + + return $traverse($type); + }); + } + + $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)) + ->identifier('method.templateTypeNotInParameter') + ->build(); + } + } + + return $errors; + } + + /** + * @param Param[] $parameterNodes + * @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(); + } + if (!is_string($parameterNode->var->name)) { + throw new ShouldNotHappenException(); + } + $parameterName = $parameterNode->var->name; + if ($optionalParameter !== null && $parameterNode->default === null && !$parameterNode->variadic) { + $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) { + continue; + } + if ($parameterNode->type === null) { + $optionalParameter = $parameterName; + continue; + } + + $defaultValue = $parameterNode->default; + if (!$defaultValue instanceof ConstFetch) { + $optionalParameter = $parameterName; + continue; + } + + $constantName = $defaultValue->name->toLowerString(); + if ($constantName === 'null') { + 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; } return $errors; } /** - * @param string $parameterName * @param Param[] $parameterNodes - * @return Param */ private function getParameterNode( string $parameterName, - array $parameterNodes + array $parameterNodes, ): Param { foreach ($parameterNodes as $param) { - if ($param->var instanceof \PhpParser\Node\Expr\Error) { + if ($param->var instanceof Node\Expr\Error) { continue; } @@ -268,16 +658,15 @@ private function getParameterNode( } } - throw new \PHPStan\ShouldNotHappenException(sprintf('Parameter %s not found.', $parameterName)); + throw new ShouldNotHappenException(sprintf('Parameter %s not found.', $parameterName)); } /** - * @param \PHPStan\Reflection\ParameterReflection $parameter * @return string[] */ private function getParameterReferencedClasses(ParameterReflection $parameter): array { - if (!$parameter instanceof ParameterReflectionWithPhpDocs) { + if (!$parameter instanceof ExtendedParameterReflection) { return $parameter->getType()->getReferencedClasses(); } @@ -285,19 +674,27 @@ 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() + $parameter->getPhpDocType()->getReferencedClasses(), + $moreClasses, ); } /** - * @param \PHPStan\Reflection\ParametersAcceptor $parametersAcceptor * @return string[] */ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAcceptor): array { - if (!$parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { + if (!$parametersAcceptor instanceof ExtendedParametersAcceptor) { return $parametersAcceptor->getReturnType()->getReferencedClasses(); } @@ -307,8 +704,65 @@ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAc return array_merge( $parametersAcceptor->getNativeReturnType()->getReferencedClasses(), - $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses() + $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses(), ); } + 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 d290c010cd..994b3943f2 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -2,61 +2,59 @@ namespace PHPStan\Rules; +use Generator; +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 { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PHPStan\Type\Type $returnType - * @param \PhpParser\Node\Expr|null $returnValue - * @param string $emptyReturnStatementMessage - * @param string $voidMessage - * @param string $typeMismatchMessage - * @param bool $isGenerator - * @return RuleError[] + * @return list */ public function checkReturnType( Scope $scope, Type $returnType, ?Expr $returnValue, + Node $returnNode, string $emptyReturnStatementMessage, string $voidMessage, string $typeMismatchMessage, - bool $isGenerator + string $neverMessage, + bool $isGenerator, ): array { - if ($isGenerator) { - if (!$returnType instanceof TypeWithClassName) { - return []; - } + $returnType = TypeUtils::resolveLateResolvableTypes($returnType); + + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + return [ + RuleErrorBuilder::message($neverMessage) + ->line($returnNode->getStartLine()) + ->identifier('return.never') + ->build(), + ]; + } - $returnType = GenericTypeVariableResolver::getType( - $returnType, - \Generator::class, - 'TReturn' - ); - if ($returnType === null) { + if ($isGenerator) { + $returnType = $returnType->getTemplateType(Generator::class, 'TReturn'); + if ($returnType instanceof ErrorType) { return []; } } - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType); + $isVoidSuperType = $returnType->isVoid(); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType, null); if ($returnValue === null) { if (!$isVoidSuperType->no()) { return []; @@ -65,29 +63,45 @@ public function checkReturnType( return [ RuleErrorBuilder::message(sprintf( $emptyReturnStatementMessage, - $returnType->describe($verbosityLevel) - ))->build(), + $returnType->describe($verbosityLevel), + )) + ->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); if ($isVoidSuperType->yes()) { return [ RuleErrorBuilder::message(sprintf( $voidMessage, - $returnValueType->describe($verbosityLevel) - ))->build(), + $returnValueType->describe($verbosityLevel), + )) + ->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) - ))->build(), + $returnValueType->describe($verbosityLevel), + )) + ->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 new file mode 100644 index 0000000000..67af9eb5e0 --- /dev/null +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -0,0 +1,37 @@ + + */ +final class ArrowFunctionAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return InArrowFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_FUNCTION, + 'function', + ); + } + +} diff --git a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php new file mode 100644 index 0000000000..cc7d673620 --- /dev/null +++ b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php @@ -0,0 +1,44 @@ + + */ +final class ArrowFunctionReturnNullsafeByRefRule implements Rule +{ + + public function __construct(private NullsafeCheck $nullsafeCheck) + { + } + + public function getNodeType(): string + { + return Node\Expr\ArrowFunction::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->byRef) { + return []; + } + + if (!$this->nullsafeCheck->containsNullSafe($node->expr)) { + return []; + } + + return [ + 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 a6239b592d..045f66913b 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -2,23 +2,24 @@ namespace PHPStan\Rules\Functions; +use Generator; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InArrowFunctionNode> + * @implements Rule */ -class ArrowFunctionReturnTypeRule implements \PHPStan\Rules\Rule +final class ArrowFunctionReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - public function __construct(FunctionReturnTypeCheck $returnTypeCheck) + public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) { - $this->returnTypeCheck = $returnTypeCheck; } public function getNodeType(): string @@ -29,21 +30,38 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInAnonymousFunction()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - /** @var \PHPStan\Type\Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); - $generatorType = new ObjectType(\Generator::class); + $generatorType = new ObjectType(Generator::class); + + $originalNode = $node->getOriginalNode(); + $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, - $node->getOriginalNode()->expr, + $originalNode->expr, + $originalNode->expr, 'Anonymous function should return %s but empty return statement found.', 'Anonymous function with return type void returns %s but should not return anything.', 'Anonymous function should return %s but returns %s.', - $generatorType->isSuperTypeOf($returnType)->yes() + 'Anonymous function should never return but return statement found.', + $generatorType->isSuperTypeOf($returnType)->yes(), ); } diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 19e762dfb3..162894e266 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -2,61 +2,59 @@ namespace PHPStan\Rules\Functions; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\InaccessibleMethod; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function count; +use function sprintf; +use function ucfirst; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class CallCallablesRule implements \PHPStan\Rules\Rule +final class CallCallablesRule implements Rule { - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - public function __construct( - FunctionCallParametersCheck $check, - RuleLevelHelper $ruleLevelHelper, - bool $reportMaybes + private FunctionCallParametersCheck $check, + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, ) { - $this->check = $check; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\FuncCall::class; + return Node\Expr\FuncCall::class; } public function processNode( - \PhpParser\Node $node, - Scope $scope + Node $node, + Scope $scope, ): array { - if (!$node->name instanceof \PhpParser\Node\Expr) { + if (!$node->name instanceof Node\Expr) { return []; } $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->name, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->name), 'Invoking callable on an unknown class %s.', - static function (Type $type): bool { - return $type->isCallable()->yes(); - } + static fn (Type $type): bool => $type->isCallable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -67,21 +65,26 @@ static function (Type $type): bool { if ($isCallable->no()) { return [ RuleErrorBuilder::message( - sprintf('Trying to invoke %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())) - )->build(), + sprintf('Trying to invoke %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), + )->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(), + sprintf('Trying to invoke %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), + )->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 @@ -91,20 +94,21 @@ static function (Type $type): bool { 'Call to %s method %s() of class %s.', $method->isPrivate() ? 'private' : 'protected', $method->getName(), - $method->getDeclaringClass()->getDisplayName() - ))->build(); + $method->getDeclaringClass()->getDisplayName(), + ))->identifier('callable.inaccessibleMethod')->build(); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $parametersAcceptors + $node->getArgs(), + $parametersAcceptors, + null, ); if ($type instanceof ClosureType) { $callableDescription = 'closure'; } else { - $callableDescription = sprintf('callable %s', $type->describe(VerbosityLevel::value())); + $callableDescription = sprintf('callable %s', SprintfHelper::escapeFormatString($type->describe(VerbosityLevel::value()))); } return array_merge( @@ -112,20 +116,26 @@ static function (Type $type): bool { $this->check->check( $parametersAcceptor, $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 #%d %s of ' . $callableDescription . ' expects %s, %s given.', - 'Result of ' . $callableDescription . ' (void) is used.', - 'Parameter #%d %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to ' . $callableDescription, - ] - ) + '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 e760ba8fa2..63e1b93c7a 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -5,24 +5,20 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class CallToFunctionParametersRule implements \PHPStan\Rules\Rule +final class CallToFunctionParametersRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - public function __construct(ReflectionProvider $reflectionProvider, FunctionCallParametersCheck $check) + public function __construct(private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; } public function getNodeType(): string @@ -32,7 +28,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } @@ -41,27 +37,35 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = SprintfHelper::escapeFormatString($function->getName()); return $this->check->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $function->getVariants() + $node->getArgs(), + $function->getVariants(), + $function->getNamedArgumentsVariants(), ), $scope, + $function->isBuiltin(), $node, - [ - 'Function ' . $function->getName() . ' invoked with %d parameter, %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameter, at least %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, at least %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameter, %d-%d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, %d-%d required.', - 'Parameter #%d %s of function ' . $function->getName() . ' expects %s, %s given.', - 'Result of function ' . $function->getName() . ' (void) is used.', - 'Parameter #%d %s of function ' . $function->getName() . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to function ' . $function->getName(), - ] + '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/CallToFunctionStamentWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRule.php deleted file mode 100644 index 4cb83d76b4..0000000000 --- a/src/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRule.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -class CallToFunctionStamentWithoutSideEffectsRule implements Rule -{ - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) - { - $this->reflectionProvider = $reflectionProvider; - } - - public function getNodeType(): string - { - return Node\Stmt\Expression::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$node->expr instanceof Node\Expr\FuncCall) { - return []; - } - - $funcCall = $node->expr; - if (!($funcCall->name instanceof \PhpParser\Node\Name)) { - return []; - } - - if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { - return []; - } - - $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - if ($function->hasSideEffects()->no()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Call to function %s() on a separate line has no effect.', - $function->getName() - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php new file mode 100644 index 0000000000..23338924eb --- /dev/null +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -0,0 +1,140 @@ + + */ +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) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Expression::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->expr instanceof Node\Expr\FuncCall) { + return []; + } + + $funcCall = $node->expr; + if (!($funcCall->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + return []; + } + + $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); + $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->isVoid()->yes()) { + return []; + } + } + + $functionResult = $scope->getType($funcCall); + if ($functionResult instanceof NeverType && $functionResult->isExplicit()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line has no effect.', + $function->getName(), + ))->identifier('function.resultUnused')->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/CallToNonExistentFunctionRule.php b/src/Rules/Functions/CallToNonExistentFunctionRule.php index 78b2b2f6e8..9805a445b8 100644 --- a/src/Rules/Functions/CallToNonExistentFunctionRule.php +++ b/src/Rules/Functions/CallToNonExistentFunctionRule.php @@ -6,25 +6,23 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class CallToNonExistentFunctionRule implements \PHPStan\Rules\Rule +final class CallToNonExistentFunctionRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string @@ -34,13 +32,24 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + if ($scope->isInFunctionExists($node->name->toString())) { + return []; + } + + $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))->build(), + $errorBuilder->build(), ]; } @@ -58,8 +67,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Call to function %s() with incorrect case: %s', $function->getName(), - $name - ))->build(), + $name, + ))->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 new file mode 100644 index 0000000000..fc206e19d4 --- /dev/null +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -0,0 +1,37 @@ + + */ +final class ClosureAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return InClosureNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_FUNCTION, + 'function', + ); + } + +} diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index 6459f47061..415da4d6bc 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -3,27 +3,25 @@ namespace PHPStan\Rules\Functions; use PhpParser\Node; -use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; +use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\FunctionReturnTypeCheck; -use PHPStan\Type\ObjectType; +use PHPStan\Rules\Rule; +use PHPStan\Type\TypeCombinator; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Return_> + * @implements Rule */ -class ClosureReturnTypeRule implements \PHPStan\Rules\Rule +final class ClosureReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - public function __construct(FunctionReturnTypeCheck $returnTypeCheck) + public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) { - $this->returnTypeCheck = $returnTypeCheck; } public function getNodeType(): string { - return Return_::class; + return ClosureReturnStatementsNode::class; } public function processNode(Node $node, Scope $scope): array @@ -32,19 +30,35 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PHPStan\Type\Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); - $generatorType = new ObjectType(\Generator::class); - - return $this->returnTypeCheck->checkReturnType( - $scope, - $returnType, - $node->expr, - 'Anonymous function should return %s but empty return statement found.', - 'Anonymous function with return type void returns %s but should not return anything.', - 'Anonymous function should return %s but returns %s.', - $generatorType->isSuperTypeOf($returnType)->yes() - ); + $containsNull = TypeCombinator::containsNull($returnType); + $hasNativeTypehint = $node->getClosureExpr()->returnType !== null; + + $messages = []; + foreach ($node->getReturnStatements() as $returnStatement) { + $returnNode = $returnStatement->getReturnNode(); + $returnExpr = $returnNode->expr; + if ($returnExpr === null && $containsNull && !$hasNativeTypehint) { + $returnExpr = new Node\Expr\ConstFetch(new Node\Name\FullyQualified('null')); + } + $returnMessages = $this->returnTypeCheck->checkReturnType( + $returnStatement->getScope(), + $returnType, + $returnExpr, + $returnNode, + 'Anonymous function should return %s but empty return statement found.', + '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.', + $node->isGenerator(), + ); + + foreach ($returnMessages as $returnMessage) { + $messages[] = $returnMessage; + } + } + + return $messages; } } diff --git a/src/Rules/Functions/ClosureUsesThisRule.php b/src/Rules/Functions/ClosureUsesThisRule.php deleted file mode 100644 index 60018f454b..0000000000 --- a/src/Rules/Functions/ClosureUsesThisRule.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class ClosureUsesThisRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Expr\Closure::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($node->static) { - return []; - } - - $messages = []; - foreach ($node->uses as $closureUse) { - $varType = $scope->getType($closureUse->var); - if (!is_string($closureUse->var->name)) { - continue; - } - if (!$varType instanceof ThisType) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf('Anonymous function uses $this assigned to variable $%s. Use $this directly in the function body.', $closureUse->var->name)) - ->line($closureUse->getLine()) - ->build(); - } - return $messages; - } - -} diff --git a/src/Rules/Functions/DefineParametersRule.php b/src/Rules/Functions/DefineParametersRule.php new file mode 100644 index 0000000000..83886f1ea2 --- /dev/null +++ b/src/Rules/Functions/DefineParametersRule.php @@ -0,0 +1,57 @@ + + */ +final class DefineParametersRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + if ($this->phpVersion->supportsCaseInsensitiveConstantNames()) { + return []; + } + $name = strtolower((string) $node->name); + if ($name !== 'define') { + return []; + } + $args = $node->getArgs(); + $argsCount = count($args); + // Expects 2 or 3, 1 arg is caught by CallToFunctionParametersRule + if ($argsCount < 3) { + return []; + } + return [ + RuleErrorBuilder::message( + 'Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported.', + ) + ->line($node->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 f5b3b1e932..0b29af004c 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -4,19 +4,22 @@ 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 \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrowFunction> + * @implements Rule */ -class ExistingClassesInArrowFunctionTypehintsRule implements \PHPStan\Rules\Rule +final class ExistingClassesInArrowFunctionTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check, private PhpVersion $phpVersion) { - $this->check = $check; } public function getNodeType(): string @@ -26,13 +29,27 @@ 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(), - 'Parameter $%s of anonymous function has invalid typehint type %s.', - 'Return typehint of anonymous function has invalid type %s.' - ); + 'Parameter $%s of anonymous function has invalid type %s.', + 'Anonymous function has invalid return type %s.', + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 'Parameter $%s of anonymous function has unresolvable native type.', + 'Anonymous function has unresolvable native return type.', + )); } } diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index 9a53fd3007..0c5acfbd07 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -6,18 +6,16 @@ use PhpParser\Node\Expr\Closure; use PHPStan\Analyser\Scope; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Closure> + * @implements Rule */ -class ExistingClassesInClosureTypehintsRule implements \PHPStan\Rules\Rule +final class ExistingClassesInClosureTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -31,8 +29,11 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getParams(), $node->getReturnType(), - 'Parameter $%s of anonymous function has invalid typehint type %s.', - 'Return typehint of anonymous function has invalid type %s.' + 'Parameter $%s of anonymous function has invalid type %s.', + 'Anonymous function has invalid return type %s.', + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 'Parameter $%s of anonymous function has unresolvable native type.', + 'Anonymous function has unresolvable native return type.', ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 92ba9159f5..7f83eea193 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -4,21 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ExistingClassesInTypehintsRule implements \PHPStan\Rules\Rule +final class ExistingClassesInTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -28,23 +27,30 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->getFunction() instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - - $functionName = $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, + ), sprintf( - 'Parameter $%%s of function %s() has invalid typehint type %%s.', - $functionName + 'Function %s() has invalid return type %%s.', + $functionName, ), + sprintf('Function %s() uses native union types but they\'re supported only on PHP 8.0 and later.', $functionName), + sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName), sprintf( - 'Return typehint of function %s() has invalid type %%s.', - $functionName - ) + 'Parameter $%%s of function %s() has unresolvable native type.', + $functionName, + ), + sprintf( + 'Function %s() has unresolvable native return type.', + $functionName, + ), ); } diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php new file mode 100644 index 0000000000..9c5ad24d73 --- /dev/null +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -0,0 +1,37 @@ + + */ +final class FunctionAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->getOriginalNode()->attrGroups, + Attribute::TARGET_FUNCTION, + 'function', + ); + } + +} diff --git a/src/Rules/Functions/FunctionCallableRule.php b/src/Rules/Functions/FunctionCallableRule.php new file mode 100644 index 0000000000..827cf6e876 --- /dev/null +++ b/src/Rules/Functions/FunctionCallableRule.php @@ -0,0 +1,113 @@ + + */ +final class FunctionCallableRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, private bool $checkFunctionNameCase, private bool $reportMaybes) + { + } + + public function getNodeType(): string + { + return FunctionCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->identifier('callable.notSupported') + ->build(), + ]; + } + + $functionName = $node->getName(); + if ($functionName instanceof Node\Name) { + $functionNameName = $functionName->toString(); + if ($this->reflectionProvider->hasFunction($functionName, $scope)) { + if ($this->checkFunctionNameCase) { + $function = $this->reflectionProvider->getFunction($functionName, $scope); + + /** @var string $calledFunctionName */ + $calledFunctionName = $this->reflectionProvider->resolveFunctionName($functionName, $scope); + if ( + strtolower($function->getName()) === strtolower($calledFunctionName) + && $function->getName() !== $calledFunctionName + ) { + return [ + RuleErrorBuilder::message(sprintf( + 'Call to function %s() with incorrect case: %s', + $function->getName(), + $functionNameName, + ))->identifier('function.nameCase')->build(), + ]; + } + } + + return []; + } + + if ($scope->isInFunctionExists($functionNameName)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Function %s not found.', $functionNameName)) + ->identifier('function.notFound') + ->build(), + ]; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $functionName), + 'Creating callable from an unknown class %s.', + static fn (Type $type): bool => $type->isCallable()->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $isCallable = $type->isCallable(); + if ($isCallable->no()) { + return [ + RuleErrorBuilder::message( + sprintf('Creating callable from %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), + )->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())), + )->identifier('callable.nonCallable')->build(), + ]; + } + + return []; + } + +} 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 713fb0a759..68f9fffc6d 100644 --- a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php @@ -5,17 +5,18 @@ 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; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InFunctionNode> + * @implements Rule */ -class IncompatibleDefaultParameterTypeRule implements Rule +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -25,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) { @@ -40,18 +36,19 @@ public function processNode(Node $node, Scope $scope): array $param->var instanceof Node\Expr\Error || !is_string($param->var->name) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $defaultValueType = $scope->getType($param->default); - $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; } - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType); $errors[] = RuleErrorBuilder::message(sprintf( 'Default value of the parameter #%d $%s (%s) of function %s() is incompatible with type %s.', @@ -59,8 +56,12 @@ public function processNode(Node $node, Scope $scope): array $param->var->name, $defaultValueType->describe($verbosityLevel), $function->getName(), - $parameterType->describe($verbosityLevel) - ))->line($param->getLine())->build(); + $parameterType->describe($verbosityLevel), + )) + ->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 5d0c0cfe93..04694b4447 100644 --- a/src/Rules/Functions/InnerFunctionRule.php +++ b/src/Rules/Functions/InnerFunctionRule.php @@ -5,12 +5,13 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Function_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_> + * @implements Rule */ -class InnerFunctionRule implements \PHPStan\Rules\Rule +final class InnerFunctionRule implements Rule { public function getNodeType(): string @@ -26,8 +27,8 @@ 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(), + '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.', + )->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 9546dd4d06..586ce8727c 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -6,27 +6,25 @@ 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\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingFunctionParameterTypehintRule implements \PHPStan\Rules\Rule +final class MissingFunctionParameterTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - public function __construct( - MissingTypehintCheck $missingTypehintCheck + private MissingTypehintCheck $missingTypehintCheck, ) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -36,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; } } @@ -53,21 +61,17 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param \PHPStan\Reflection\FunctionReflection $functionReflection - * @param \PHPStan\Reflection\ParameterReflection $parameterReflection - * @return \PHPStan\Rules\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 typehint specified.', + 'Function %s() has %s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName() - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -75,21 +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(), - $iterableTypeDescription - ))->tip(sprintf(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, $iterableTypeDescription))->build(); + $parameterMessage, + $iterableTypeDescription, + )) + ->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 %s with no signature specified for %s.', + $functionReflection->getName(), + $parameterMessage, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index e20f8563e4..648636973e 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -5,26 +5,23 @@ 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 sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingFunctionReturnTypehintRule implements \PHPStan\Rules\Rule +final class MissingFunctionReturnTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - public function __construct( - MissingTypehintCheck $missingTypehintCheck + private MissingTypehintCheck $missingTypehintCheck, ) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -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 typehint specified.', - $functionReflection->getName() - ))->build(), + 'Function %s() has no return type specified.', + $functionReflection->getName(), + ))->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(sprintf(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, $iterableTypeDescription))->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,18 @@ 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) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Function %s() return type has no signature specified for %s.', + $functionReflection->getName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php new file mode 100644 index 0000000000..02006c1619 --- /dev/null +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -0,0 +1,43 @@ + + */ +final class ParamAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return Node\Param::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $targetName = 'parameter'; + $targetType = Attribute::TARGET_PARAMETER; + if ($node->flags !== 0) { + $targetName = 'parameter or property'; + $targetType |= Attribute::TARGET_PROPERTY; + } + + return $this->attributesCheck->check( + $scope, + $node->attrGroups, + $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 2b6bc3aeb5..a80b44b995 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -5,15 +5,40 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function count; +use function in_array; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class PrintfParametersRule implements \PHPStan\Rules\Rule +final class PrintfParametersRule implements Rule { + 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, + ) + { + } + public function getNodeType(): string { return FuncCall::class; @@ -21,107 +46,73 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { 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])) { + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $name = $functionReflection->getName(); + if (!array_key_exists($name, self::FORMAT_ARGUMENT_POSITIONS)) { return []; } - $formatArgumentPosition = $functionsArgumentPositions[$name]; + $formatArgumentPosition = self::FORMAT_ARGUMENT_POSITIONS[$name]; - $args = $node->args; + $args = $node->getArgs(); foreach ($args as $arg) { if ($arg->unpack) { return []; } } $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', - $argsCount - 1 === 1 ? '%d value given' : '%d values given' + $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, - $argsCount - 1 - ))->build(), + $maxPlaceHoldersCount, + $argsCount - 1, + ))->identifier(sprintf('argument.%s', $name))->build(), ]; } return []; } - private function getPlaceholdersCount(string $functionName, string $format): int - { - $specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '[bcdeEfFgGosuxX]' : '(?:[cdDeEfinosuxX]|\[[^\]]+\])'; - $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?-?\d*(?:\.\d*)?' . $specifiers . '~'; - - $matches = \Nette\Utils\Strings::matchAll($format, $pattern, PREG_SET_ORDER); - - if (count($matches) === 0) { - return 0; - } - - $placeholders = array_filter($matches, static function (array $match): bool { - return strlen($match['before']) % 2 === 0; - }); - - 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 c63971e7dc..e35c21a3ed 100644 --- a/src/Rules/Functions/RandomIntParametersRule.php +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -5,27 +5,29 @@ 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\Constant\ConstantIntegerType; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\VerbosityLevel; +use function array_values; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class RandomIntParametersRule implements \PHPStan\Rules\Rule +final class RandomIntParametersRule implements Rule { - private ReflectionProvider $reflectionProvider; - - private bool $reportMaybes; - - public function __construct(ReflectionProvider $reflectionProvider, bool $reportMaybes) + public function __construct( + private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, + private bool $reportMaybes, + ) { - $this->reflectionProvider = $reflectionProvider; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -35,7 +37,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } @@ -43,37 +45,32 @@ public function processNode(Node $node, Scope $scope): array return []; } - $minType = $scope->getType($node->args[0]->value)->toInteger(); - $maxType = $scope->getType($node->args[1]->value)->toInteger(); - $integerType = new IntegerType(); - - if ($minType->equals($integerType) || $maxType->equals($integerType)) { + $args = array_values($node->getArgs()); + if (count($args) < 2) { return []; } - if ($minType instanceof ConstantIntegerType || $minType instanceof IntegerRangeType) { - if ($minType instanceof ConstantIntegerType) { - $maxPermittedType = IntegerRangeType::fromInterval($minType->getValue(), PHP_INT_MAX); - } else { - $maxPermittedType = IntegerRangeType::fromInterval($minType->getMax(), PHP_INT_MAX); - } + $minType = $scope->getType($args[0]->value)->toInteger(); + $maxType = $scope->getType($args[1]->value)->toInteger(); - if (!$maxPermittedType->isSuperTypeOf($maxType)->yes()) { - $message = 'Parameter #1 $min (%s) of function random_int expects lower number than parameter #2 $max (%s).'; - - // True if sometimes the parameters conflict. - $isMaybe = !$maxType->isSuperTypeOf($minType)->no(); + if ( + !$minType instanceof ConstantIntegerType && !$minType instanceof IntegerRangeType + || !$maxType instanceof ConstantIntegerType && !$maxType instanceof IntegerRangeType + ) { + return []; + } - if (!$isMaybe || $this->reportMaybes) { - return [ - RuleErrorBuilder::message(sprintf( - $message, - $minType->describe(VerbosityLevel::value()), - $maxType->describe(VerbosityLevel::value()) - ))->build(), - ]; - } - } + $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).'; + return [ + RuleErrorBuilder::message(sprintf( + $message, + $minType->describe(VerbosityLevel::value()), + $maxType->describe(VerbosityLevel::value()), + ))->identifier('argument.type')->build(), + ]; } return []; 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 new file mode 100644 index 0000000000..f90bdca70f --- /dev/null +++ b/src/Rules/Functions/ReturnNullsafeByRefRule.php @@ -0,0 +1,54 @@ + + */ +final class ReturnNullsafeByRefRule implements Rule +{ + + public function __construct(private NullsafeCheck $nullsafeCheck) + { + } + + public function getNodeType(): string + { + return ReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->returnsByRef()) { + return []; + } + + $errors = []; + foreach ($node->getReturnStatements() as $returnStatement) { + $returnNode = $returnStatement->getReturnNode(); + if ($returnNode->expr === null) { + continue; + } + + if (!$this->nullsafeCheck->containsNullSafe($returnNode->expr)) { + continue; + } + + $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 22da0789b9..9fdd33f14a 100644 --- a/src/Rules/Functions/ReturnTypeRule.php +++ b/src/Rules/Functions/ReturnTypeRule.php @@ -5,30 +5,21 @@ 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 Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; -use Roave\BetterReflection\Reflector\FunctionReflector; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Return_> + * @implements Rule */ -class ReturnTypeRule implements \PHPStan\Rules\Rule +final class ReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - private FunctionReflector $functionReflector; - public function __construct( - FunctionReturnTypeCheck $returnTypeCheck, - FunctionReflector $functionReflector + private FunctionReturnTypeCheck $returnTypeCheck, ) { - $this->returnTypeCheck = $returnTypeCheck; - $this->functionReflector = $functionReflector; } public function getNodeType(): string @@ -47,41 +38,32 @@ public function processNode(Node $node, Scope $scope): array } $function = $scope->getFunction(); - if ( - !($function instanceof PhpFunctionFromParserNodeReflection) - || $function instanceof PhpMethodFromParserNodeReflection - ) { + if ($function instanceof MethodReflection) { return []; } - $reflection = null; - if (function_exists($function->getName())) { - $reflection = new \ReflectionFunction($function->getName()); - } else { - try { - $reflection = $this->functionReflector->reflect($function->getName()); - } catch (IdentifierNotFound $e) { - // pass - } - } - return $this->returnTypeCheck->checkReturnType( $scope, - ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(), + $function->getReturnType(), $node->expr, + $node, sprintf( 'Function %s() should return %%s but empty return statement found.', - $function->getName() + $function->getName(), ), sprintf( 'Function %s() with return type void returns %%s but should not return anything.', - $function->getName() + $function->getName(), ), sprintf( 'Function %s() should return %%s but returns %%s.', - $function->getName() + $function->getName(), + ), + sprintf( + 'Function %s() should never return but return statement found.', + $function->getName(), ), - $reflection !== null && $reflection->isGenerator() + $function->isGenerator(), ); } 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 85441063a6..ed9639e41f 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -4,19 +4,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use function array_map; +use function count; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Closure> + * @implements Rule */ -class UnusedClosureUsesRule implements \PHPStan\Rules\Rule +final class UnusedClosureUsesRule implements Rule { - private \PHPStan\Rules\UnusedFunctionParametersCheck $check; - - public function __construct(UnusedFunctionParametersCheck $check) + public function __construct(private UnusedFunctionParametersCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -32,15 +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 \PHPStan\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' + '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 99d638e19d..5bac445f13 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -2,35 +2,29 @@ namespace PHPStan\Rules\Generators; +use Generator; 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 function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\YieldFrom> + * @implements Rule */ -class YieldFromTypeRule implements Rule +final class YieldFromTypeRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - bool $reportMaybes + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -47,8 +41,11 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( $messagePattern, - $exprType->describe(VerbosityLevel::typeOnly()) - ))->line($node->expr->getLine())->build(), + $exprType->describe(VerbosityLevel::typeOnly()), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.nonIterable') + ->build(), ]; } elseif ( !$exprType instanceof MixedType @@ -58,8 +55,11 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( $messagePattern, - $exprType->describe(VerbosityLevel::typeOnly()) - ))->line($node->expr->getLine())->build(), + $exprType->describe(VerbosityLevel::typeOnly()), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.nonIterable') + ->build(), ]; } @@ -68,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 } @@ -78,21 +78,32 @@ public function processNode(Node $node, Scope $scope): array } $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes())) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType()); + $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(); + $exprType->getIterableKeyType()->describe($verbosityLevel), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.keyType') + ->acceptsReasonsTip($acceptsKey->reasons) + ->build(); } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes())) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType()); + + $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(); + $exprType->getIterableValueType()->describe($verbosityLevel), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.valueType') + ->acceptsReasonsTip($acceptsValue->reasons) + ->build(); } $scopeFunction = $scope->getFunction(); @@ -100,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; } @@ -120,14 +123,20 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), - $thisSendType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $thisSendType->describe(VerbosityLevel::typeOnly()), + ))->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(); + $thisSendType->describe(VerbosityLevel::typeOnly()), + ))->identifier('generator.sendType')->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 e87072f25e..c6283e8bb5 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -4,24 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; -use PHPStan\Type\ArrayType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class YieldInGeneratorRule implements Rule +final class YieldInGeneratorRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct(private bool $reportMaybes) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -40,18 +37,27 @@ 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) { return []; } - $isSuperType = $returnType->isIterable()->and(TrinaryLogic::createFromBoolean( - $returnType instanceof ArrayType - )->negate()); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $isSuperType = TrinaryLogic::createNo(); + } else { + $isSuperType = $returnType->isIterable()->and(TrinaryLogic::createFromBoolean( + !$returnType->isArray()->yes(), + )); + } if ($isSuperType->yes()) { return []; } @@ -63,8 +69,8 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Yield can be used only with these return types: %s.', - 'Generator, Iterator, Traversable, iterable' - ))->build(), + 'Generator, Iterator, Traversable, iterable', + ))->identifier('generator.returnType')->build(), ]; } diff --git a/src/Rules/Generators/YieldTypeRule.php b/src/Rules/Generators/YieldTypeRule.php index ec853cd40e..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,20 +11,18 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Yield_> + * @implements Rule */ -class YieldTypeRule implements Rule +final class YieldTypeRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -40,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 } @@ -55,28 +52,42 @@ public function processNode(Node $node, Scope $scope): array $keyType = $scope->getType($node->key); } + $messages = []; + $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), + )) + ->acceptsReasonsTip($acceptsKey->reasons) + ->identifier('generator.keyType') + ->build(); + } + if ($node->value === null) { $valueType = new NullType(); } else { $valueType = $scope->getType($node->value); } - $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes())) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType()); - $messages[] = RuleErrorBuilder::message(sprintf( - 'Generator expects key type %s, %s given.', - $returnType->getIterableKeyType()->describe($verbosityLevel), - $keyType->describe($verbosityLevel) - ))->build(); - } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes())) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType()); + $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(); + $valueType->describe($verbosityLevel), + )) + ->acceptsReasonsTip($acceptsValue->reasons) + ->identifier('generator.valueType') + ->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 d6087dbfe7..a0a07a2c20 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -4,94 +4,85 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_map; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ -class ClassAncestorsRule implements Rule +final class ClassAncestorsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; } public function getNodeType(): string { - return Node\Stmt\Class_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - // anonymous class + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof Node\Stmt\Class_) { return []; } - - $className = (string) $node->namespacedName; - - $extendsTags = []; - $implementsTags = []; - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $className, - null, - null, - $docComment->getText() - ); - $extendsTags = $resolvedPhpDoc->getExtendsTags(); - $implementsTags = $resolvedPhpDoc->getImplementsTags(); + $classReflection = $node->getClassReflection(); + if ($classReflection->isAnonymous()) { + return []; } + $className = $classReflection->getName(); + $escapedClassName = SprintfHelper::escapeFormatString($className); $extendsErrors = $this->genericAncestorsCheck->check( - $node->extends !== null ? [$node->extends] : [], - array_map(static function (ExtendsTag $tag): Type { - return $tag->getType(); - }, $extendsTags), - sprintf('Class %s @extends tag contains incompatible type %%s.', $className), - sprintf('Class %s has @extends tag, but does not extend any class.', $className), - sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $className), - 'PHPDoc tag @extends contains generic type %s but class %s is not generic.', - 'Generic type %s in PHPDoc tag @extends does not specify all template types of class %s: %s', - 'Generic type %s in PHPDoc tag @extends specifies %d template types, but class %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of class %s.', + $originalNode->extends !== null ? [$originalNode->extends] : [], + array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), + sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s @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', $className), - sprintf('in extended type %%s of class %s', $className) + sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName), + sprintf('in extended type %%s of class %s', $escapedClassName), ); $implementsErrors = $this->genericAncestorsCheck->check( - $node->implements, - array_map(static function (ImplementsTag $tag): Type { - return $tag->getType(); - }, $implementsTags), - sprintf('Class %s @implements tag contains incompatible type %%s.', $className), - sprintf('Class %s has @implements tag, but does not implement any interface.', $className), - sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $className), - 'PHPDoc tag @implements contains generic type %s but interface %s is not generic.', - 'Generic type %s in PHPDoc tag @implements does not specify all template types of interface %s: %s', - 'Generic type %s in PHPDoc tag @implements specifies %d template types, but interface %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of interface %s.', + $originalNode->implements, + array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), + sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s @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', $className), - sprintf('in implemented type %%s of class %s', $className) + sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName), + sprintf('in implemented type %%s of class %s', $escapedClassName), ); + foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { + $implementsErrors[] = $error; + } + return array_merge($extendsErrors, $implementsErrors); } diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index 1b4e1ea618..f574d76460 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -4,62 +4,54 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ -class ClassTemplateTypeRule implements Rule +final class ClassTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string { - return Node\Stmt\Class_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isClass()) { return []; } - - if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + $className = $classReflection->getName(); + if ($classReflection->isAnonymous()) { + $displayName = 'anonymous class'; + } else { + $displayName = 'class ' . SprintfHelper::escapeFormatString($classReflection->getDisplayName()); } - $className = (string) $node->namespacedName; - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $className, - null, - null, - $docComment->getText() - ); - return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($className), - $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for class %s cannot have existing class %%s as its name.', $className), - sprintf('PHPDoc tag @template for class %s cannot have existing type alias %%s as its name.', $className), - sprintf('PHPDoc tag @template %%s for class %s has invalid bound type %%s.', $className), - sprintf('PHPDoc tag @template %%s for class %s with bound type %%s is not supported.', $className) + $classReflection->getTemplateTags(), + sprintf('PHPDoc tag @template for %s cannot have existing class %%s as its name.', $displayName), + sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName), + sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName), + sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName), + sprintf('PHPDoc tag @template %%s for %s 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 new file mode 100644 index 0000000000..3e9c477cb3 --- /dev/null +++ b/src/Rules/Generics/CrossCheckInterfacesHelper.php @@ -0,0 +1,94 @@ + + */ + public function check(ClassReflection $classReflection): array + { + $interfaceTemplateTypeMaps = []; + $errors = []; + $check = static function (ClassReflection $classReflection, bool $first) use (&$interfaceTemplateTypeMaps, &$check, &$errors): void { + foreach ($classReflection->getInterfaces() as $interface) { + if (!$interface->isGeneric()) { + continue; + } + + if (array_key_exists($interface->getName(), $interfaceTemplateTypeMaps)) { + $otherMap = $interfaceTemplateTypeMaps[$interface->getName()]; + foreach ($interface->getActiveTemplateTypeMap()->getTypes() as $name => $type) { + $otherType = $otherMap->getType($name); + if ($otherType === null) { + continue; + } + + if ($type->equals($otherType)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s specifies template type %s of interface %s as %s but it\'s already specified as %s.', + $classReflection->isInterface() ? sprintf('Interface %s', $classReflection->getName()) : sprintf('Class %s', $classReflection->getName()), + $name, + $interface->getName(), + $type->describe(VerbosityLevel::value()), + $otherType->describe(VerbosityLevel::value()), + ))->identifier('generics.interfaceConflict')->build(); + } + continue; + } + + $interfaceTemplateTypeMaps[$interface->getName()] = $interface->getActiveTemplateTypeMap(); + } + + $parent = $classReflection->getParentClass(); + $checkParents = true; + if ($first && $parent !== null) { + $extendsTags = $classReflection->getExtendsTags(); + if (!array_key_exists($parent->getName(), $extendsTags)) { + $checkParents = false; + } + } + + if ($checkParents) { + while ($parent !== null) { + $check($parent, false); + $parent = $parent->getParentClass(); + } + } + + $interfaceTags = []; + if ($first) { + if ($classReflection->isInterface()) { + $interfaceTags = $classReflection->getExtendsTags(); + } else { + $interfaceTags = $classReflection->getImplementsTags(); + } + } + foreach ($classReflection->getInterfaces() as $interface) { + if ($first) { + if (!array_key_exists($interface->getName(), $interfaceTags)) { + continue; + } + } + $check($interface, false); + } + }; + + $check($classReflection, true); + + return $errors; + } + +} diff --git a/src/Rules/Generics/EnumAncestorsRule.php b/src/Rules/Generics/EnumAncestorsRule.php new file mode 100644 index 0000000000..71daff135b --- /dev/null +++ b/src/Rules/Generics/EnumAncestorsRule.php @@ -0,0 +1,87 @@ + + */ +final class EnumAncestorsRule implements Rule +{ + + public function __construct( + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof Node\Stmt\Enum_) { + return []; + } + $classReflection = $node->getClassReflection(); + + $enumName = $classReflection->getName(); + $escapedEnumName = SprintfHelper::escapeFormatString($enumName); + + $extendsErrors = $this->genericAncestorsCheck->check( + [], + array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), + sprintf('Enum %s @extends tag contains incompatible type %%s.', $escapedEnumName), + sprintf('Enum %s @extends tag contains unresolvable type.', $enumName), + sprintf('Enum %s has @extends tag, but cannot extend anything.', $escapedEnumName), + '', + '', + '', + '', + '', + '', + '', + '', + '', + ); + + $implementsErrors = $this->genericAncestorsCheck->check( + $originalNode->implements, + array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), + sprintf('Enum %s @implements tag contains incompatible type %%s.', $escapedEnumName), + sprintf('Enum %s @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), + ); + + foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { + $implementsErrors[] = $error; + } + + return array_merge($extendsErrors, $implementsErrors); + } + +} diff --git a/src/Rules/Generics/EnumTemplateTypeRule.php b/src/Rules/Generics/EnumTemplateTypeRule.php new file mode 100644 index 0000000000..6f5b049c7b --- /dev/null +++ b/src/Rules/Generics/EnumTemplateTypeRule.php @@ -0,0 +1,45 @@ + + */ +final class EnumTemplateTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isEnum()) { + return []; + } + + $templateTagsCount = count($classReflection->getTemplateTags()); + if ($templateTagsCount === 0) { + return []; + } + + $className = $classReflection->getDisplayName(); + + return [ + RuleErrorBuilder::message(sprintf('Enum %s has PHPDoc @template tag%s but enums cannot be generic.', $className, $templateTagsCount === 1 ? '' : 's')) + ->identifier('enum.generic') + ->build(), + ]; + } + +} diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index 84ae32af92..65e0f7bca3 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -4,21 +4,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class FunctionSignatureVarianceRule implements Rule +final class FunctionSignatureVarianceRule implements Rule { - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - public function __construct(VarianceCheck $varianceCheck) + public function __construct(private VarianceCheck $varianceCheck) { - $this->varianceCheck = $varianceCheck; } public function getNodeType(): string @@ -28,18 +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()), - sprintf('in parameter %%s of function %s()', $functionName), + $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), - false + sprintf('in function %s()', $functionName), + false, + false, + 'function', ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 2a6d2bc921..d4b56da5f2 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -4,27 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_> + * @implements Rule */ -class FunctionTemplateTypeRule implements Rule +final class FunctionTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -40,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array } if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionName = (string) $node->namespacedName; @@ -49,17 +46,23 @@ public function processNode(Node $node, Scope $scope): array null, null, $functionName, - $docComment->getText() + $docComment->getText(), ); + $escapedFunctionName = SprintfHelper::escapeFormatString($functionName); + return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithFunction($functionName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $functionName), - sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $functionName), - sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $functionName), - sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $functionName) + sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $escapedFunctionName), + sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName), + 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 928156ede0..d55fd57116 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -2,77 +2,90 @@ namespace PHPStan\Rules\Generics; +use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\MissingTypehintCheck; +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; - -class GenericAncestorsCheck +use function array_fill_keys; +use function array_filter; +use function array_keys; +use function array_map; +use function array_merge; +use function count; +use function implode; +use function in_array; +use function sprintf; + +final class GenericAncestorsCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - private bool $checkGenericClassInNonGenericObjectType; - + /** + * @param string[] $skipCheckGenericClasses + */ public function __construct( - ReflectionProvider $reflectionProvider, - GenericObjectTypeCheck $genericObjectTypeCheck, - VarianceCheck $varianceCheck, - bool $checkGenericClassInNonGenericObjectType + private ReflectionProvider $reflectionProvider, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private VarianceCheck $varianceCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private array $skipCheckGenericClasses, + private bool $checkMissingTypehints, ) { - $this->reflectionProvider = $reflectionProvider; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->varianceCheck = $varianceCheck; - $this->checkGenericClassInNonGenericObjectType = $checkGenericClassInNonGenericObjectType; } /** - * @param array<\PhpParser\Node\Name> $nameNodes - * @param array<\PHPStan\Type\Type> $ancestorTypes - * @return \PHPStan\Rules\RuleError[] + * @param array $nameNodes + * @param array $ancestorTypes + * @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 + string $invalidVarianceMessage, ): array { - $names = array_fill_keys(array_map(static function (Name $nameNode): string { - return $nameNode->toString(); - }, $nameNodes), true); + $names = array_fill_keys(array_map(static fn (Name $nameNode): string => $nameNode->toString(), $nameNodes), true); $unusedNames = $names; $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; @@ -85,47 +98,95 @@ public function check( $classNotGenericMessage, $notEnoughTypesMessage, $extraTypesMessage, - $typeIsNotSubtypeMessage + $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) - && !$this->reflectionProvider->getClass($referencedClass)->isTrait() - ) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('class.notFound') + ->build(); + continue; + } + + if ($referencedClass === $ancestorType->getClassName()) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { continue; } - $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('generics.trait') + ->build(); } - $variance = TemplateTypeVariance::createInvariant(); + $variance = TemplateTypeVariance::createStatic(); $messageContext = sprintf( $invalidVarianceMessage, - $ancestorType->describe(VerbosityLevel::typeOnly()) + $ancestorType->describe(VerbosityLevel::typeOnly()), ); 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; } $unusedNameClassReflection = $this->reflectionProvider->getClass($unusedName); + if (in_array($unusedNameClassReflection->getName(), $this->skipCheckGenericClasses, true)) { + continue; + } if (!$unusedNameClassReflection->isGeneric()) { 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 2a9e43afd3..c0a4936bdc 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -2,31 +2,41 @@ namespace PHPStan\Rules\Generics; +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 { /** - * @param \PHPStan\Type\Type $phpDocType - * @param string $classNotGenericMessage - * @param string $notEnoughTypesMessage - * @param string $extraTypesMessage - * @param string $typeIsNotSubtypeMessage - * @return \PHPStan\Rules\RuleError[] + * @return list */ public function check( Type $phpDocType, string $classNotGenericMessage, string $notEnoughTypesMessage, string $extraTypesMessage, - string $typeIsNotSubtypeMessage + string $typeIsNotSubtypeMessage, + string $typeProjectionHasConflictingVarianceMessage, + string $typeProjectionIsRedundantMessage, ): array { $genericTypes = $this->getGenericTypes($phpDocType); @@ -36,45 +46,109 @@ public function check( if ($classReflection === null) { continue; } + + $classLikeDescription = strtolower($classReflection->getClassTypeDescription()); if (!$classReflection->isGeneric()) { - $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName()))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName())) + ->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()), $genericTypeTypesCount, + $classLikeDescription, $classReflection->getDisplayName(false), $templateTypesCount, - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())) - ))->build(); + $templateTypesList, + ))->identifier('generics.moreTypes')->build(); } - foreach ($templateTypes as $i => $templateType) { + for ($i = 0; $i < $templateTypesCount; $i++) { if (!isset($genericTypeTypes[$i])) { continue; } - $boundType = $templateType; - if ($templateType instanceof TemplateType) { - $boundType = $templateType->getBound(); - } + $templateType = $templateTypes[$i]; $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; + } + $map = $templateType->inferTemplateTypes($genericTypeType); + for ($j = 0; $j < $templateTypesCount; $j++) { + if ($i === $j) { + continue; + } + + $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes( + $templateTypes[$j], + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + ); + } + continue; + } + + if ($genericTypeVariance->bivariant()) { continue; } @@ -83,8 +157,9 @@ public function check( $genericTypeType->describe(VerbosityLevel::typeOnly()), $genericType->describe(VerbosityLevel::typeOnly()), $templateType->describe(VerbosityLevel::typeOnly()), - $classReflection->getDisplayName(false) - ))->build(); + $classLikeDescription, + $classReflection->getDisplayName(false), + ))->identifier('generics.notSubtype')->build(); } } @@ -92,27 +167,19 @@ public function check( } /** - * @param \PHPStan\Type\Type $phpDocType - * @return \PHPStan\Type\Generic\GenericObjectType[] + * @return list */ private function getGenericTypes(Type $phpDocType): array { - if ($phpDocType instanceof GenericObjectType) { - $resolvedType = TemplateTypeHelper::resolveToBounds($phpDocType); - if (!$resolvedType instanceof GenericObjectType) { - throw new \PHPStan\ShouldNotHappenException(); - } - return [$resolvedType]; - } - $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) { - throw new \PHPStan\ShouldNotHappenException(); + if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) { + throw new ShouldNotHappenException(); } $genericObjectTypes[] = $resolvedType; + $traverse($type); return $type; } $traverse($type); diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index 5a7f294113..c270de3626 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -4,82 +4,70 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_map; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> + * @implements Rule */ -class InterfaceAncestorsRule implements Rule +final class InterfaceAncestorsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; } public function getNodeType(): string { - return Node\Stmt\Interface_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof Node\Stmt\Interface_) { + return []; } + $classReflection = $node->getClassReflection(); - $interfaceName = (string) $node->namespacedName; - $extendsTags = []; - $implementsTags = []; - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $interfaceName, - null, - null, - $docComment->getText() - ); - $extendsTags = $resolvedPhpDoc->getExtendsTags(); - $implementsTags = $resolvedPhpDoc->getImplementsTags(); - } + $interfaceName = $classReflection->getName(); + $escapedInterfaceName = SprintfHelper::escapeFormatString($interfaceName); $extendsErrors = $this->genericAncestorsCheck->check( - $node->extends, - array_map(static function (ExtendsTag $tag): Type { - return $tag->getType(); - }, $extendsTags), - sprintf('Interface %s @extends tag contains incompatible type %%s.', $interfaceName), - sprintf('Interface %s has @extends tag, but does not extend any interface.', $interfaceName), - sprintf('The @extends tag of interface %s describes %%s but the interface extends: %%s', $interfaceName), - 'PHPDoc tag @extends contains generic type %s but interface %s is not generic.', - 'Generic type %s in PHPDoc tag @extends does not specify all template types of interface %s: %s', - 'Generic type %s in PHPDoc tag @extends specifies %d template types, but interface %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of interface %s.', + $originalNode->extends, + array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), + sprintf('Interface %s @extends tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s @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', $interfaceName), - sprintf('in extended type %%s of interface %s', $interfaceName) + sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $escapedInterfaceName), + sprintf('in extended type %%s of interface %s', $escapedInterfaceName), ); $implementsErrors = $this->genericAncestorsCheck->check( [], - array_map(static function (ImplementsTag $tag): Type { - return $tag->getType(); - }, $implementsTags), - sprintf('Interface %s @implements tag contains incompatible type %%s.', $interfaceName), - sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $interfaceName), + array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), + sprintf('Interface %s @implements tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s @implements tag contains unresolvable type.', $interfaceName), + sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $escapedInterfaceName), + '', + '', '', '', '', @@ -87,9 +75,12 @@ public function processNode(Node $node, Scope $scope): array '', '', '', - '' ); + foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { + $implementsErrors[] = $error; + } + return array_merge($extendsErrors, $implementsErrors); } diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 87f8b8cded..53adafb43a 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -4,62 +4,51 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> + * @implements Rule */ -class InterfaceTemplateTypeRule implements Rule +final class InterfaceTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string { - return Node\Stmt\Interface_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isInterface()) { return []; } + $interfaceName = $classReflection->getName(); - if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $interfaceName = (string) $node->namespacedName; - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $interfaceName, - null, - null, - $docComment->getText() - ); + $escapadInterfaceName = SprintfHelper::escapeFormatString($interfaceName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($interfaceName), - $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $interfaceName), - sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $interfaceName), - sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $interfaceName), - sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $interfaceName) + $classReflection->getTemplateTags(), + sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $escapadInterfaceName), + sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName), + 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 b2c478d547..44543983dc 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -4,22 +4,19 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class MethodSignatureVarianceRule implements Rule +final class MethodSignatureVarianceRule implements Rule { - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - public function __construct(VarianceCheck $varianceCheck) + public function __construct(private VarianceCheck $varianceCheck) { - $this->varianceCheck = $varianceCheck; } public function getNodeType(): string @@ -29,16 +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()), - sprintf('in parameter %%s of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + $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()), - $method->getName() === '__construct' || $method->isStatic() + sprintf('in method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + $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 57ac147a2c..65653f833f 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -4,29 +4,27 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\VerbosityLevel; +use function array_keys; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ClassMethod> + * @implements Rule */ -class MethodTemplateTypeRule implements Rule +final class MethodTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -42,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -50,21 +48,27 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->name->toString(); $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), - $className, + $classReflection->getName(), $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $methodName, - $docComment->getText() + $docComment->getText(), ); $methodTemplateTags = $resolvedPhpDoc->getTemplateTags(); + $escapedClassName = SprintfHelper::escapeFormatString($className); + $escapedMethodName = SprintfHelper::escapeFormatString($methodName); $messages = $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithMethod($className, $methodName), $methodTemplateTags, - sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $className, $methodName), - sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $className, $methodName), - sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $className, $methodName), - sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $className, $methodName) + sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + 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 76e63952af..27be48dd50 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,114 +3,212 @@ 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\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; +use PHPStan\Type\TypeAliasResolver; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use function array_key_exists; use function array_map; +use function array_merge; +use function get_class; +use function sprintf; -class TemplateTypeCheck +final class TemplateTypeCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - /** @var array */ - private array $typeAliases; - - private bool $checkClassCaseSensitivity; - - /** - * @param ReflectionProvider $reflectionProvider - * @param ClassCaseSensitivityCheck $classCaseSensitivityCheck - * @param array $typeAliases - * @param bool $checkClassCaseSensitivity - */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - array $typeAliases, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private TypeAliasResolver $typeAliasResolver, + private bool $checkClassCaseSensitivity, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->typeAliases = $typeAliases; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } /** - * @param \PhpParser\Node $node - * @param \PHPStan\Type\Generic\TemplateTypeScope $templateTypeScope - * @param array $templateTags - * @return \PHPStan\Rules\RuleError[] + * @param array $templateTags + * @return list */ public function check( + Scope $scope, Node $node, TemplateTypeScope $templateTypeScope, array $templateTags, string $sameTemplateTypeNameAsClassMessage, string $sameTemplateTypeNameAsTypeMessage, string $invalidBoundTypeMessage, - string $notSupportedBoundMessage + 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(); + $templateTagName, + ))->identifier('generics.existingClass')->build(); } - if (array_key_exists($templateTagName, $this->typeAliases)) { + if ($this->typeAliasResolver->hasTypeAlias($templateTagName, $templateTypeScope->getClassName())) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsTypeMessage, - $templateTagName - ))->build(); + $templateTagName, + ))->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; } $messages[] = RuleErrorBuilder::message(sprintf( $invalidBoundTypeMessage, $templateTagName, - $referencedClass - ))->build(); + $referencedClass, + ))->identifier('generics.traitBound')->build(); } - if ($this->checkClassCaseSensitivity) { - $classNameNodePairs = array_map(static function (string $referencedClass) use ($node): ClassNameNodePair { - return 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)); - $bound = $templateTag->getBound(); - $boundClass = get_class($bound); + $boundTypeClass = get_class($boundType); if ( - $boundClass === MixedType::class - || $boundClass === ObjectWithoutClassType::class - || $bound instanceof ObjectType + $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()))) + ->identifier('generics.notSupportedBound') + ->build(); + } + + $escapedTemplateTagName = SprintfHelper::escapeFormatString($templateTagName); + $genericObjectErrors = $this->genericObjectTypeCheck->check( + $boundType, + sprintf('PHPDoc tag @template %s bound contains generic type %%s but %%s %%s is not generic.', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of %%s %%s: %%s', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTemplateTagName), + sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of %%s %%s.', $escapedTemplateTagName), + 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($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly())))->build(); + $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 21b89339cd..27ce74e298 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -4,27 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Trait_> + * @implements Rule */ -class TraitTemplateTypeRule implements Rule +final class TraitTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -40,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array } if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $traitName = (string) $node->namespacedName; @@ -49,17 +46,23 @@ public function processNode(Node $node, Scope $scope): array $traitName, null, null, - $docComment->getText() + $docComment->getText(), ); + $escapedTraitName = SprintfHelper::escapeFormatString($traitName); + return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($traitName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $traitName), - sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $traitName), - sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $traitName), - sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $traitName) + sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $escapedTraitName), + sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName), + 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 new file mode 100644 index 0000000000..ef698a4061 --- /dev/null +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -0,0 +1,89 @@ + + */ +final class UsedTraitsRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + private GenericAncestorsCheck $genericAncestorsCheck, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\TraitUse::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $className = $scope->getClassReflection()->getName(); + $traitName = null; + if ($scope->isInTrait()) { + $traitName = $scope->getTraitReflection()->getName(); + } + $useTags = []; + $docComment = $node->getDocComment(); + if ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $className, + $traitName, + null, + $docComment->getText(), + ); + $useTags = $resolvedPhpDoc->getUsesTags(); + } + + $typeDescription = strtolower($scope->getClassReflection()->getClassTypeDescription()); + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className)); + if ($traitName !== null) { + $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.', $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', $escapedUpperCaseDescription), + sprintf('in used type %%s of %s', $escapedDescription), + ); + } + +} diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index d268031919..d01dbf75a3 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -2,54 +2,91 @@ 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, - bool $isStatic + string $generalMessage, + bool $isStatic, + bool $isPrivate, + string $identifier, ): array { $errors = []; + foreach ($parametersAcceptor->getTemplateTypeMap()->getTypes() as $templateType) { + if (!$templateType instanceof TemplateType + || $templateType->getScope()->getFunctionName() === null + || $templateType->getVariance()->invariant() + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type %s in %s.', + $templateType->getName(), + $generalMessage, + ))->identifier(sprintf('%s.variance', $identifier))->build(); + } + + if ($isPrivate) { + return $errors; + } + + $covariant = TemplateTypeVariance::createCovariant(); + $parameterVariance = TemplateTypeVariance::createContravariant(); + foreach ($parametersAcceptor->getParameters() as $parameterReflection) { - $variance = $isStatic - ? TemplateTypeVariance::createStatic() - : TemplateTypeVariance::createContravariant(); $type = $parameterReflection->getType(); $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); - foreach ($this->check($variance, $type, $message) as $error) { + foreach ($this->check($parameterVariance, $type, $message) as $error) { + $errors[] = $error; + } + + $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 = []; foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) { $referredType = $reference->getType(); - if ($this->isTemplateTypeVarianceValid($reference->getPositionVariance(), $referredType)) { + if (($referredType->getScope()->getFunctionName() !== null && !$referredType->getVariance()->invariant()) + || $this->isTemplateTypeVarianceValid($reference->getPositionVariance(), $referredType)) { continue; } @@ -58,8 +95,8 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string $referredType->getName(), $referredType->getVariance()->describe(), $reference->getPositionVariance()->describe(), - $messageContext - ))->build(); + $messageContext, + ))->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 6cfb74c7a9..9669fccc8a 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -5,75 +5,111 @@ 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\NullType; +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 { - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - public function __construct( - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkAdvancedIsset, + private bool $treatPhpDocTypesAsCertain, ) { - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; } - public function check(Expr $expr, Scope $scope, string $operatorDescription, ?RuleError $error = null): ?RuleError + /** + * @param ErrorIdentifier $identifier + * @param callable(Type): ?string $typeMessageCallback + */ + 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)) { $hasVariable = $scope->hasVariableType($expr->name); if ($hasVariable->maybe()) { return null; } + if ($error === null) { + if ($hasVariable->yes()) { + if ($expr->name === '_SESSION') { + return null; + } + + $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)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); + } + return $error; } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - - $type = $scope->getType($expr->var); - $dimType = $scope->getType($expr->dim); - $hasOffsetValue = $type->hasOffsetValueType($dimType); + $type = $this->treatPhpDocTypesAsCertain + ? $scope->getType($expr->var) + : $scope->getNativeType($expr->var); if (!$type->isOffsetAccessible()->yes()) { - return $error; + 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 ($hasOffsetValue->no()) { - return $error ?? RuleErrorBuilder::message( + if (!$this->checkAdvancedIsset) { + return null; + } + + return RuleErrorBuilder::message( sprintf( 'Offset %s on %s %s does not exist.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), - $operatorDescription - ) - )->build(); - } - - if ($hasOffsetValue->maybe()) { - return null; + $operatorDescription, + ), + )->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 ($hasOffsetValue->yes() || $scope->hasExpressionType($expr)->yes()) { + if (!$this->checkAdvancedIsset) { + return null; + } - $error = $error ?? $this->generateError($type->getOffsetValueType($dimType), sprintf( + $error ??= $this->generateError($type->getOffsetValueType($dimType), sprintf( 'Offset %s on %s %s always exists and', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), - $operatorDescription - )); + $operatorDescription, + ), $typeMessageCallback, $identifier, 'offset'); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -85,61 +121,210 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, ?Ru $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope); if ($propertyReflection === null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + + if ($expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); + } + return null; } if (!$propertyReflection->isNative()) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + + if ($expr->class instanceof Expr) { + 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, $identifier); + } + + if ($expr->class instanceof Expr) { + 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, $identifier); + } - $error = $error ?? $this->generateError( + if ($expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); + } + + return null; + } + + $error = $this->generateError( $propertyReflection->getWritableType(), - sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription) + sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription), + $typeMessageCallback, + $identifier, + 'property', ); if ($error !== null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $error); + return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } return $error; } - return $error ?? $this->generateError($scope->getType($expr), sprintf('Expression %s', $operatorDescription)); + if ($error !== null) { + return $error; + } + + if (!$this->checkAdvancedIsset) { + return null; + } + + $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 generateError(Type $type, string $message): ?RuleError + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError { - $nullType = new NullType(); + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + $hasVariable = $scope->hasVariableType($expr->name); + if (!$hasVariable->no()) { + return null; + } - if ($type->equals($nullType)) { - return RuleErrorBuilder::message( - sprintf('%s is always null.', $message) - )->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } - if ($type->isSuperTypeOf($nullType)->no()) { + if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + $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, $identifier); + } + + if (!$hasOffsetValue->no()) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + return RuleErrorBuilder::message( - sprintf('%s is not nullable.', $message) - )->build(); + sprintf( + 'Offset %s on %s %s does not exist.', + $dimType->describe(VerbosityLevel::value()), + $type->describe(VerbosityLevel::value()), + $operatorDescription, + ), + )->identifier(sprintf('%s.offset', $identifier))->build(); + } + + if ($expr instanceof Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; } + /** + * @param callable(Type): ?string $typeMessageCallback + * @param ErrorIdentifier $identifier + * @param 'variable'|'offset'|'property'|'expr'|'initializedProperty' $identifierSecondPart + */ + private function generateError(Type $type, string $message, callable $typeMessageCallback, string $identifier, string $identifierSecondPart): ?IdentifierRuleError + { + $typeMessage = $typeMessageCallback($type); + if ($typeMessage === null) { + return null; + } + + return RuleErrorBuilder::message( + sprintf('%s %s.', $message, $typeMessage), + )->identifier(sprintf('%s.%s', $identifier, $identifierSecondPart))->build(); + } + } diff --git a/src/Rules/Keywords/ContinueBreakInLoopRule.php b/src/Rules/Keywords/ContinueBreakInLoopRule.php new file mode 100644 index 0000000000..4f421e5a6c --- /dev/null +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -0,0 +1,82 @@ + + */ +final class ContinueBreakInLoopRule implements Rule +{ + + public function getNodeType(): string + { + return Stmt::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Stmt\Continue_ && !$node instanceof Stmt\Break_) { + return []; + } + + if (!$node->num instanceof Node\Scalar\Int_) { + $value = 1; + } else { + $value = $node->num->value; + } + + $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() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), + ]; + } + if ( + $parentStmtType === Stmt\For_::class + || $parentStmtType === Stmt\Foreach_::class + || $parentStmtType === Stmt\Do_::class + || $parentStmtType === Stmt\While_::class + || $parentStmtType === Stmt\Switch_::class + ) { + $value--; + } + if ($value === 0) { + break; + } + } + + 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 d197147d5c..82c92a4ecd 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -6,11 +6,14 @@ use PHPStan\Analyser\Scope; 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 @@ -21,21 +24,52 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $class = $scope->getClassReflection(); - 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 @@ + + * @implements Rule */ -class CallMethodsRule implements \PHPStan\Rules\Rule +final class CallMethodsRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $checkFunctionNameCase; - - private bool $reportMagicMethods; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $check, - RuleLevelHelper $ruleLevelHelper, - bool $checkFunctionNameCase, - bool $reportMagicMethods + private MethodCallCheck $methodCallCheck, + private FunctionCallParametersCheck $parametersCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->checkFunctionNameCase = $checkFunctionNameCase; - $this->reportMagicMethods = $reportMagicMethods; } public function getNodeType(): string @@ -52,121 +34,70 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; - } - - $name = $node->name->name; - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->var, - sprintf('Call to method %s() on an unknown class %%s.', $name), - static function (Type $type) use ($name): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($name)->yes(); + $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))); } - ); - $type = $typeResult->getType(); - if ($type instanceof ErrorType) { - return $typeResult->getUnknownClassErrors(); } - if (!$type->canCallMethods()->yes()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot call method %s() on %s.', - $name, - $type->describe(VerbosityLevel::typeOnly()) - ))->build(), - ]; - } - - if (!$type->hasMethod($name)->yes()) { - $directClassNames = $typeResult->getReferencedClasses(); - if (!$this->reportMagicMethods) { - foreach ($directClassNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasNativeMethod('__call')) { - return []; - } - } - } - if (count($directClassNames) === 1) { - $referencedClass = $directClassNames[0]; - $methodClassReflection = $this->reflectionProvider->getClass($referencedClass); - $parentClassReflection = $methodClassReflection->getParentClass(); - while ($parentClassReflection !== false) { - if ($parentClassReflection->hasMethod($name)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Call to private method %s() of parent class %s.', - $parentClassReflection->getMethod($name, $scope)->getName(), - $parentClassReflection->getDisplayName() - ))->build(), - ]; - } + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); + } - $parentClassReflection = $parentClassReflection->getParentClass(); - } - } + return $errors; + } - return [ - RuleErrorBuilder::message(sprintf( - 'Call to an undefined method %s::%s().', - $type->describe(VerbosityLevel::typeOnly()), - $name - ))->build(), - ]; + /** + * @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; } - $methodReflection = $type->getMethod($name, $scope); - $messagesMethodName = $methodReflection->getDeclaringClass()->getDisplayName() . '::' . $methodReflection->getName() . '()'; - $errors = []; - if (!$scope->canCallMethod($methodReflection)) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Call to %s method %s() of class %s.', - $methodReflection->isPrivate() ? 'private' : 'protected', - $methodReflection->getName(), - $methodReflection->getDeclaringClass()->getDisplayName() - ))->build(); - } + $declaringClass = $methodReflection->getDeclaringClass(); + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors = array_merge($errors, $this->check->check( + return array_merge($errors, $this->parametersCheck->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $methodReflection->getVariants() + $node->getArgs(), + $methodReflection->getVariants(), + $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 #%d %s of method ' . $messagesMethodName . ' expects %s, %s given.', - 'Result of method ' . $messagesMethodName . ' (void) is used.', - 'Parameter #%d %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, - ] + '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.', )); - - if ( - $this->checkFunctionNameCase - && strtolower($methodReflection->getName()) === strtolower($name) - && $methodReflection->getName() !== $name - ) { - $errors[] = RuleErrorBuilder::message( - sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $name) - )->build(); - } - - return $errors; } } diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php new file mode 100644 index 0000000000..d381cdc659 --- /dev/null +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -0,0 +1,62 @@ + + */ +final class CallPrivateMethodThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $methodName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasMethod($methodName)->yes()) { + return []; + } + + $method = $classType->getMethod($methodName, $scope); + if (!$method->isPrivate()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe call to private method %s::%s() through static::.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('staticClassAccess.privateMethod')->build(), + ]; + } + +} diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index fd94d4fdc2..04ffc64325 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -3,62 +3,29 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\StaticCall; -use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; -use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\FunctionCallParametersCheck; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\StringType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\VerbosityLevel; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticCall> + * @implements Rule */ -class CallStaticMethodsRule implements \PHPStan\Rules\Rule +final class CallStaticMethodsRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkFunctionNameCase; - - private bool $reportMagicMethods; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $check, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkFunctionNameCase, - bool $reportMagicMethods + private StaticMethodCallCheck $methodCallCheck, + private FunctionCallParametersCheck $parametersCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkFunctionNameCase = $checkFunctionNameCase; - $this->reportMagicMethods = $reportMagicMethods; } public function getNodeType(): string @@ -68,242 +35,79 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; - } - $methodName = $node->name->name; - - $class = $node->class; $errors = []; - $isAbstract = false; - if ($class instanceof Name) { - $className = (string) $class; - $lowercasedClassName = strtolower($className); - if (in_array($lowercasedClassName, ['self', 'static'], true)) { - if (!$scope->isInClass()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Calling %s::%s() outside of class scope.', - $className, - $methodName - ))->build(), - ]; - } - $classReflection = $scope->getClassReflection(); - } elseif ($lowercasedClassName === 'parent') { - if (!$scope->isInClass()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Calling %s::%s() outside of class scope.', - $className, - $methodName - ))->build(), - ]; - } - $currentClassReflection = $scope->getClassReflection(); - if ($currentClassReflection->getParentClass() === false) { - return [ - RuleErrorBuilder::message(sprintf( - '%s::%s() calls parent::%s() but %s does not extend any class.', - $scope->getClassReflection()->getDisplayName(), - $scope->getFunctionName(), - $methodName, - $scope->getClassReflection()->getDisplayName() - ))->build(), - ]; - } - - if ($scope->getFunctionName() === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classReflection = $currentClassReflection->getParentClass(); - } else { - if (!$this->reflectionProvider->hasClass($className)) { - if ($scope->isInClassExists($className)) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Call to static method %s() on an unknown class %s.', - $methodName, - $className - ))->build(), - ]; - } else { - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->isTrait()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Call to static method %s() on trait %s.', - $methodName, - $className - ))->build(), - ]; - } - } - - $className = $classReflection->getName(); - $classType = new ObjectType($className); - - if ($classReflection->hasNativeMethod($methodName) && $lowercasedClassName !== 'static') { - $nativeMethodReflection = $classReflection->getNativeMethod($methodName); - if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { - $isAbstract = $nativeMethodReflection->isAbstract(); - } - } + if ($node->name instanceof Node\Identifier) { + $methodNameScopes = [$node->name->name => $scope]; } else { - $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $class, - sprintf('Call to static method %s() on an unknown class %%s.', $methodName), - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } - ); - $classType = $classTypeResult->getType(); - if ($classType instanceof ErrorType) { - return $classTypeResult->getUnknownClassErrors(); - } - } - - if ($classType instanceof GenericClassStringType) { - $classType = $classType->getGenericType(); - } elseif ((new StringType())->isSuperTypeOf($classType)->yes()) { - return []; - } - - $typeForDescribe = $classType; - $classType = TypeCombinator::remove($classType, new StringType()); - - if (!$classType->canCallMethods()->yes()) { - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Cannot call static method %s() on %s.', - $methodName, - $typeForDescribe->describe(VerbosityLevel::typeOnly()) - ))->build(), - ]); - } - - if (!$classType->hasMethod($methodName)->yes()) { - if (!$this->reportMagicMethods) { - $directClassNames = TypeUtils::getDirectClassNames($classType); - foreach ($directClassNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasNativeMethod('__callStatic')) { - return []; - } - } + $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))); } - - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Call to an undefined static method %s::%s().', - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $methodName - ))->build(), - ]); } - $method = $classType->getMethod($methodName, $scope); - if (!$method->isStatic()) { - $function = $scope->getFunction(); - if ( - !$function instanceof MethodReflection - || $function->isStatic() - || !$scope->isInClass() - || ( - $classType instanceof TypeWithClassName - && $scope->getClassReflection()->getName() !== $classType->getClassName() - && !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) - ) - ) { - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Static call to instance method %s::%s().', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]); - } + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall( + $methodScope, + $node, + (string) $methodName, // @phpstan-ignore cast.useless + )); } - if (!$scope->canCallMethod($method)) { - $errors = array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s %s() of class %s.', - $method->isPrivate() ? 'private' : 'protected', - $method->isStatic() ? 'static method' : 'method', - $method->getName(), - $method->getDeclaringClass()->getDisplayName() - ))->build(), - ]); - } + return $errors; + } - if ($isAbstract) { - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot call abstract%s method %s::%s().', - $method->isStatic() ? ' static' : '', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]; + /** + * @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; } - $lowercasedMethodName = sprintf( - '%s %s', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - ); - $displayMethodName = sprintf( + $displayMethodName = SprintfHelper::escapeFormatString(sprintf( '%s %s', $method->isStatic() ? 'Static method' : 'Method', - $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - ); + $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()', + )); + $lowercasedMethodName = SprintfHelper::escapeFormatString(sprintf( + '%s %s', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()', + )); - $errors = array_merge($errors, $this->check->check( + $errors = array_merge($errors, $this->parametersCheck->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $method->getVariants() + $node->getArgs(), + $method->getVariants(), + $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 #%d %s of ' . $lowercasedMethodName . ' expects %s, %s given.', - 'Result of ' . $lowercasedMethodName . ' (void) is used.', - 'Parameter #%d %s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to method ' . $lowercasedMethodName, - ] + '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.', )); - if ( - $this->checkFunctionNameCase - && $method->getName() !== $methodName - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Call to %s with incorrect case: %s', - $lowercasedMethodName, - $methodName - ))->build(); - } - return $errors; } diff --git a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php new file mode 100644 index 0000000000..f214edf960 --- /dev/null +++ b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php @@ -0,0 +1,72 @@ + + */ +final class CallToConstructorStatementWithoutSideEffectsRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return NoopExpressionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $instantiation = $node->getOriginalExpr(); + if (!$instantiation instanceof Node\Expr\New_) { + return []; + } + + if (!$instantiation->class instanceof Node\Name) { + return []; + } + + $className = $scope->resolveName($instantiation->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstructor()) { + return [ + RuleErrorBuilder::message(sprintf( + 'Call to new %s() on a separate line has no effect.', + $classReflection->getDisplayName(), + ))->identifier('new.resultUnused')->build(), + ]; + } + + $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/CallToMethodStamentWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStamentWithoutSideEffectsRule.php deleted file mode 100644 index f8d0398990..0000000000 --- a/src/Rules/Methods/CallToMethodStamentWithoutSideEffectsRule.php +++ /dev/null @@ -1,78 +0,0 @@ - - */ -class CallToMethodStamentWithoutSideEffectsRule implements Rule -{ - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) - { - $this->ruleLevelHelper = $ruleLevelHelper; - } - - public function getNodeType(): string - { - return Node\Stmt\Expression::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$node->expr instanceof Node\Expr\MethodCall) { - return []; - } - - $methodCall = $node->expr; - if (!$methodCall->name instanceof Node\Identifier) { - return []; - } - $methodName = $methodCall->name->toString(); - - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $methodCall->var, - '', - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } - ); - $calledOnType = $typeResult->getType(); - if ($calledOnType instanceof ErrorType) { - return []; - } - if (!$calledOnType->canCallMethods()->yes()) { - return []; - } - - if (!$calledOnType->hasMethod($methodName)->yes()) { - return []; - } - - $method = $calledOnType->getMethod($methodName, $scope); - if ($method->hasSideEffects()->no()) { - 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(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php new file mode 100644 index 0000000000..c8ead1d217 --- /dev/null +++ b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php @@ -0,0 +1,81 @@ + + */ +final class CallToMethodStatementWithoutSideEffectsRule implements Rule +{ + + public function __construct(private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + return NoopExpressionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $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 []; + } + + if (!$methodCall->name instanceof Node\Identifier) { + return []; + } + $methodName = $methodCall->name->toString(); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $methodCall->var), + '', + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + $calledOnType = $typeResult->getType(); + if ($calledOnType instanceof ErrorType) { + return []; + } + if (!$calledOnType->canCallMethods()->yes()) { + return []; + } + + if (!$calledOnType->hasMethod($methodName)->yes()) { + return []; + } + + $methodResult = $scope->getType($methodCall); + if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { + 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/CallToStaticMethodStamentWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRule.php deleted file mode 100644 index da9b105e72..0000000000 --- a/src/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRule.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ -class CallToStaticMethodStamentWithoutSideEffectsRule implements Rule -{ - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct( - RuleLevelHelper $ruleLevelHelper, - ReflectionProvider $reflectionProvider - ) - { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reflectionProvider = $reflectionProvider; - } - - public function getNodeType(): string - { - return Node\Stmt\Expression::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$node->expr instanceof Node\Expr\StaticCall) { - return []; - } - - $staticCall = $node->expr; - if (!$staticCall->name instanceof Node\Identifier) { - return []; - } - - $methodName = $staticCall->name->toString(); - if ($staticCall->class instanceof Node\Name) { - $className = $scope->resolveName($staticCall->class); - if (!$this->reflectionProvider->hasClass($className)) { - return []; - } - - $calledOnType = new ObjectType($className); - } else { - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $staticCall->class, - '', - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } - ); - $calledOnType = $typeResult->getType(); - if ($calledOnType instanceof ErrorType) { - return []; - } - } - - if (!$calledOnType->canCallMethods()->yes()) { - return []; - } - - if (!$calledOnType->hasMethod($methodName)->yes()) { - return []; - } - - $method = $calledOnType->getMethod($methodName, $scope); - if ($method->hasSideEffects()->no()) { - 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(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php new file mode 100644 index 0000000000..550ea9e019 --- /dev/null +++ b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php @@ -0,0 +1,103 @@ + + */ +final class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return NoopExpressionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $staticCall = $node->getOriginalExpr(); + if (!$staticCall instanceof Node\Expr\StaticCall) { + return []; + } + + if (!$staticCall->name instanceof Node\Identifier) { + return []; + } + + $methodName = $staticCall->name->toString(); + if ($staticCall->class instanceof Node\Name) { + $className = $scope->resolveName($staticCall->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $calledOnType = new ObjectType($className); + } else { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $staticCall->class), + '', + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + $calledOnType = $typeResult->getType(); + if ($calledOnType instanceof ErrorType) { + return []; + } + } + + if (!$calledOnType->canCallMethods()->yes()) { + return []; + } + + if (!$calledOnType->hasMethod($methodName)->yes()) { + return []; + } + + $method = $calledOnType->getMethod($methodName, $scope); + if ( + ( + strtolower($method->getName()) === '__construct' + || strtolower($method->getName()) === strtolower($method->getDeclaringClass()->getName()) + ) + ) { + 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(), + ))->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 4afc837ce4..6127bac985 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -4,21 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InClassMethodNode> + * @implements Rule */ -class ExistingClassesInTypehintsRule implements \PHPStan\Rules\Rule +final class ExistingClassesInTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -28,27 +27,41 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); + $className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()); + $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); return $this->check->checkClassMethod( + $scope, $methodReflection, $node->getOriginalNode(), sprintf( - 'Parameter $%%s of method %s::%s() has invalid typehint type %%s.', - $scope->getClassReflection()->getDisplayName(), - $methodReflection->getName() + 'Parameter $%%s of method %s::%s() has invalid type %%s.', + $className, + $methodName, + ), + sprintf( + 'Method %s::%s() has invalid return type %%s.', + $className, + $methodName, + ), + sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $className, $methodName), + sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $className, $methodName), + sprintf( + 'Parameter $%%s of method %s::%s() has unresolvable native type.', + $className, + $methodName, + ), + sprintf( + 'Method %s::%s() has unresolvable native return type.', + $className, + $methodName, ), sprintf( - 'Return typehint of method %s::%s() has invalid type %%s.', - $scope->getClassReflection()->getDisplayName(), - $methodReflection->getName() - ) + '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 097150c3cc..85059abcac 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -5,17 +5,18 @@ 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; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InClassMethodNode> + * @implements Rule */ -class IncompatibleDefaultParameterTypeRule implements Rule +final class IncompatibleDefaultParameterTypeRule implements Rule { public function getNodeType(): string @@ -25,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) { @@ -41,18 +36,20 @@ public function processNode(Node $node, Scope $scope): array $param->var instanceof Node\Expr\Error || !is_string($param->var->name) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $defaultValueType = $scope->getType($param->default); - $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; } - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType); $errors[] = RuleErrorBuilder::message(sprintf( 'Default value of the parameter #%d $%s (%s) of method %s::%s() is incompatible with type %s.', @@ -61,8 +58,12 @@ public function processNode(Node $node, Scope $scope): array $defaultValueType->describe($verbosityLevel), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $parameterType->describe($verbosityLevel) - ))->line($param->getLine())->build(); + $parameterType->describe($verbosityLevel), + )) + ->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 new file mode 100644 index 0000000000..433931baf3 --- /dev/null +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -0,0 +1,37 @@ + + */ +final class MethodAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::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/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php new file mode 100644 index 0000000000..06cbf2e9ca --- /dev/null +++ b/src/Rules/Methods/MethodCallCheck.php @@ -0,0 +1,165 @@ +, ExtendedMethodReflection|null} + */ + public function check( + Scope $scope, + string $methodName, + Expr $var, + Identifier|Expr $astName, + ): array + { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $var), + sprintf('Call to method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)), + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return [$typeResult->getUnknownClassErrors(), null]; + } + + $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, + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('method.nonObject')->build(), + ], + null, + ]; + } + + if (!$type->hasMethod($methodName)->yes()) { + $directClassNames = $typeResult->getReferencedClasses(); + if (!$this->reportMagicMethods) { + foreach ($directClassNames as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->hasNativeMethod('__call')) { + return [[], null]; + } + } + } + + if (count($directClassNames) === 1) { + $referencedClass = $directClassNames[0]; + $methodClassReflection = $this->reflectionProvider->getClass($referencedClass); + $parentClassReflection = $methodClassReflection->getParentClass(); + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasMethod($methodName)) { + $methodReflection = $parentClassReflection->getMethod($methodName, $scope); + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Call to private method %s() of parent class %s.', + $methodReflection->getName(), + $parentClassReflection->getDisplayName(), + ))->identifier('method.private')->build(), + ], + $methodReflection, + ]; + } + + $parentClassReflection = $parentClassReflection->getParentClass(); + } + } + + 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().', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + $methodName, + ))->identifier('method.notFound')->build(), + ], + null, + ]; + } + + $methodReflection = $type->getMethod($methodName, $scope); + $declaringClass = $methodReflection->getDeclaringClass(); + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + $errors = []; + if (!$scope->canCallMethod($methodReflection)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s method %s() of class %s.', + $methodReflection->isPrivate() ? 'private' : 'protected', + $methodReflection->getName(), + $declaringClass->getDisplayName(), + )) + ->identifier(sprintf('method.%s', $methodReflection->isPrivate() ? 'private' : 'protected')) + ->build(); + } + + if ( + $this->checkFunctionNameCase + && strtolower($methodReflection->getName()) === strtolower($methodName) + && $methodReflection->getName() !== $methodName + ) { + $errors[] = RuleErrorBuilder::message( + sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $methodName), + )->identifier('method.nameCase')->build(); + } + + return [$errors, $methodReflection]; + } + +} diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php new file mode 100644 index 0000000000..b91b0537bf --- /dev/null +++ b/src/Rules/Methods/MethodCallableRule.php @@ -0,0 +1,66 @@ + + */ +final class MethodCallableRule implements Rule +{ + + public function __construct(private MethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return MethodCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->identifier('callable.notSupported') + ->build(), + ]; + } + + $methodName = $node->getName(); + if (!$methodName instanceof Node\Identifier) { + return []; + } + + $methodNameName = $methodName->toString(); + + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar(), $node->getName()); + if ($methodReflection === null) { + return $errors; + } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->hasNativeMethod($methodNameName)) { + return $errors; + } + + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName)) + ->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 af9bc2b862..2e585ff43e 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -6,33 +6,45 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +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; use PHPStan\Type\Type; +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 \PHPStan\Rules\Rule + * @implements Rule */ -class MethodSignatureRule implements \PHPStan\Rules\Rule +final class MethodSignatureRule implements Rule { - private bool $reportMaybes; - - private bool $reportStatic; - public function __construct( - bool $reportMaybes, - bool $reportStatic + private PhpClassReflectionExtension $phpClassReflectionExtension, + private bool $reportMaybes, + private bool $reportStatic, ) { - $this->reportMaybes = $reportMaybes; - $this->reportStatic = $reportStatic; } public function getNodeType(): string @@ -42,11 +54,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - return []; - } - + $method = $node->getMethodReflection(); $methodName = $method->getName(); if ($methodName === '__construct') { return []; @@ -57,51 +65,76 @@ public function processNode(Node $node, Scope $scope): array if ($method->isPrivate()) { return []; } - $parameters = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $errors = []; - foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass(), $scope) as $parentMethod) { - $parentParameters = ParametersAcceptorSelector::selectFromTypes(array_map(static function (ParameterReflection $parameter): Type { - return $parameter->getType(); - }, $parameters->getParameters()), $parentMethod->getVariants(), false); - - $returnTypeCompatibility = $this->checkReturnTypeCompatibility($parameters->getReturnType(), $parentParameters->getReturnType()); + $declaringClass = $method->getDeclaringClass(); + foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { + $parentVariants = $parentMethod->getVariants(); + if (count($parentVariants) !== 1) { + continue; + } + $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()', - $parameters->getReturnType()->describe(VerbosityLevel::value()), + $returnType->describe(VerbosityLevel::value()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), $returnTypeCompatibility->no() ? 'compatible' : 'covariant', - $parentParameters->getReturnType()->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), - $parentMethod->getName() - ))->build(); + $parentReturnType->describe(VerbosityLevel::value()), + $parentMethodDeclaringClass->getDisplayName(), + $parentMethod->getName(), + ))->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($parameters->getParameters(), $parentParameters->getParameters()); - foreach ($parameterResults as $parameterIndex => $parameterResult) { + $parameterResults = $this->checkParameterTypeCompatibility($declaringClass, $method->getParameters(), $parentVariant->getParameters()); + foreach ($parameterResults as $parameterIndex => [$parameterResult, $parameterType, $parentParameterType]) { if ($parameterResult->yes()) { continue; } 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, $parameter->getName(), - $parameter->getType()->describe(VerbosityLevel::value()), + $parameterType->describe(VerbosityLevel::value()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), $parameterResult->no() ? 'compatible' : 'contravariant', $parentParameter->getName(), - $parentParameter->getType()->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), - $parentMethod->getName() - ))->build(); + $parentParameterType->describe(VerbosityLevel::value()), + $parentMethodDeclaringClass->getDisplayName(), + $parentMethod->getName(), + ))->identifier('method.childParameterType')->build(); } } @@ -109,60 +142,99 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param string $methodName - * @param \PHPStan\Reflection\ClassReflection $class - * @param \PHPStan\Analyser\Scope $scope - * @return \PHPStan\Reflection\MethodReflection[] + * @return list */ - private function collectParentMethods(string $methodName, ClassReflection $class, Scope $scope): array + private function collectParentMethods(string $methodName, ClassReflection $class): array { $parentMethods = []; $parentClass = $class->getParentClass(); - if ($parentClass !== false && $parentClass->hasMethod($methodName)) { - $parentMethod = $parentClass->getMethod($methodName, $scope); + if ($parentClass !== null && $parentClass->hasNativeMethod($methodName)) { + $parentMethod = $parentClass->getNativeMethod($methodName); if (!$parentMethod->isPrivate()) { - $parentMethods[] = $parentMethod; + $parentMethods[] = [$parentMethod, $parentMethod->getDeclaringClass()]; } } foreach ($class->getInterfaces() as $interface) { - if (!$interface->hasMethod($methodName)) { + if (!$interface->hasNativeMethod($methodName)) { continue; } - $parentMethods[] = $interface->getMethod($methodName, $scope); + $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; } + /** + * @return array{TrinaryLogic, Type, Type} + */ private function checkReturnTypeCompatibility( - Type $returnType, - Type $parentReturnType - ): TrinaryLogic + ClassReflection $declaringClass, + ExtendedParametersAcceptor $currentVariant, + ExtendedParametersAcceptor $parentVariant, + ): array { + $returnType = TypehintHelper::decideType( + $currentVariant->getNativeReturnType(), + TemplateTypeHelper::resolveToBounds($currentVariant->getPhpDocReturnType()), + ); + $originalParentReturnType = TypehintHelper::decideType( + $parentVariant->getNativeReturnType(), + TemplateTypeHelper::resolveToBounds($parentVariant->getPhpDocReturnType()), + ); + $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) { - return TrinaryLogic::createYes(); + if ($returnType->isVoid()->yes() && $parentReturnType instanceof MixedType) { + return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } // We can return anything - if ($parentReturnType instanceof VoidType) { - return TrinaryLogic::createYes(); + if ($parentReturnType->isVoid()->yes()) { + return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } - return $parentReturnType->isSuperTypeOf($returnType); + return [$parentReturnType->isSuperTypeOf($returnType)->result, TypehintHelper::decideType( + $currentVariant->getNativeReturnType(), + $currentVariant->getPhpDocReturnType(), + ), $originalParentReturnType]; } /** - * @param \PHPStan\Reflection\ParameterReflection[] $parameters - * @param \PHPStan\Reflection\ParameterReflection[] $parentParameters - * @return array + * @param ExtendedParameterReflection[] $parameters + * @param ExtendedParameterReflection[] $parentParameters + * @return array */ private function checkParameterTypeCompatibility( + ClassReflection $declaringClass, array $parameters, - array $parentParameters + array $parentParameters, ): array { $parameterResults = []; @@ -172,13 +244,48 @@ private function checkParameterTypeCompatibility( $parameter = $parameters[$i]; $parentParameter = $parentParameters[$i]; - $parameterType = $parameter->getType(); - $parentParameterType = $parentParameter->getType(); + $parameterType = TypehintHelper::decideType( + $parameter->getNativeType(), + TemplateTypeHelper::resolveToBounds($parameter->getPhpDocType()), + ); + $originalParameterType = TypehintHelper::decideType( + $parentParameter->getNativeType(), + TemplateTypeHelper::resolveToBounds($parentParameter->getPhpDocType()), + ); + $parentParameterType = $this->transformStaticType($declaringClass, $originalParameterType); - $parameterResults[] = $parameterType->isSuperTypeOf($parentParameterType); + $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType)->result, TypehintHelper::decideType( + $parameter->getNativeType(), + $parameter->getPhpDocType(), + ), $originalParameterType]; } return $parameterResults; } + 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()); + } else { + $changedType = $type->changeBaseClass($declaringClass); + } + return $traverse($changedType); + } + + return $traverse($type); + }); + } + } 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 1657b7a909..ec6d40f0a3 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -4,15 +4,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; +use function sprintf; /** * @implements Rule */ -class MissingMethodImplementationRule implements Rule +final class MissingMethodImplementationRule implements Rule { public function getNodeType(): string @@ -33,27 +34,30 @@ public function processNode(Node $node, Scope $scope): array $messages = []; try { - $nativeMethods = $classReflection->getNativeMethods(); - } catch (IdentifierNotFound $e) { + $nativeMethods = $classReflection->getNativeReflection()->getMethods(); + } catch (IdentifierNotFound) { return []; } foreach ($nativeMethods as $method) { - if (!method_exists($method, 'isAbstract')) { - continue; - } if (!$method->isAbstract()) { continue; } $declaringClass = $method->getDeclaringClass(); + $classLikeDescription = 'Non-abstract class'; + if ($classReflection->isEnum()) { + $classLikeDescription = 'Enum'; + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Non-abstract class %s contains abstract method %s() from %s %s.', + '%s %s contains abstract method %s() from %s %s.', + $classLikeDescription, $classReflection->getDisplayName(), $method->getName(), $declaringClass->isInterface() ? 'interface' : 'class', - $declaringClass->getDisplayName() - ))->nonIgnorable()->build(); + $declaringClass->getName(), + ))->nonIgnorable()->identifier('method.abstract')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 778ced2ec9..5c6c204760 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -6,24 +6,25 @@ 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\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingMethodParameterTypehintRule implements \PHPStan\Rules\Rule +final class MissingMethodParameterTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + ) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -33,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; } } @@ -50,22 +61,18 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param \PHPStan\Reflection\MethodReflection $methodReflection - * @param \PHPStan\Reflection\ParameterReflection $parameterReflection - * @return \PHPStan\Rules\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 typehint specified.', + 'Method %s::%s() has %s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName() - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -73,23 +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(), - $iterableTypeDescription - ))->tip(sprintf(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, $iterableTypeDescription))->build(); + $parameterMessage, + $iterableTypeDescription, + )) + ->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 %s with no signature specified for %s.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $parameterMessage, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index 11002b0218..2b3c563f2d 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -5,24 +5,21 @@ 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 sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingMethodReturnTypehintRule implements \PHPStan\Rules\Rule +final class MissingMethodReturnTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct(private MissingTypehintCheck $missingTypehintCheck) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -32,20 +29,23 @@ 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 [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has no return typehint specified.', + 'Method %s::%s() has no return type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), - $methodReflection->getName() - ))->build(), + $methodReflection->getName(), + ))->identifier('missingType.return')->build(), ]; } @@ -56,8 +56,11 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() return type has no value type specified in iterable type %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $iterableTypeDescription - ))->tip(sprintf(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, $iterableTypeDescription))->build(); + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -66,8 +69,19 @@ 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) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() return type has no signature specified for %s.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->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 new file mode 100644 index 0000000000..e950e4cb9a --- /dev/null +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -0,0 +1,37 @@ + + */ +final class NullsafeMethodCallRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\NullsafeMethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $calledOnType = $scope->getType($node->var); + if (!$calledOnType->isNull()->no()) { + return []; + } + + return [ + 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 cf2969f0da..2dcb9d3cc3 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -6,43 +6,40 @@ 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\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 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 { - private PhpVersion $phpVersion; - - private MethodSignatureRule $methodSignatureRule; - - private bool $checkPhpDocMethodSignatures; - public function __construct( - PhpVersion $phpVersion, - MethodSignatureRule $methodSignatureRule, - bool $checkPhpDocMethodSignatures + private PhpVersion $phpVersion, + private MethodSignatureRule $methodSignatureRule, + private bool $checkPhpDocMethodSignatures, + private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, + private PhpClassReflectionExtension $phpClassReflectionExtension, + private bool $checkMissingOverrideMethodAttribute, ) { - $this->phpVersion = $phpVersion; - $this->methodSignatureRule = $methodSignatureRule; - $this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures; } public function getNodeType(): string @@ -52,47 +49,95 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - throw new \PHPStan\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 !== false && $parent->hasConstructor()) { + 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(), - $parentConstructor->getName() - ))->nonIgnorable()->build(), + $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(), + ))->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(), - $prototype->getName() - ))->nonIgnorable()->build(); + $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(), + ))->identifier('method.parentMethodFinalByPhpDoc') + ->build(); } if ($prototype->isStatic()) { @@ -101,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(), - $prototype->getName() - ))->nonIgnorable()->build(); + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->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(), - $prototype->getName() - ))->nonIgnorable()->build(); + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->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(); @@ -143,170 +177,75 @@ public function processNode(Node $node, Scope $scope): array $prototypeVariant = $prototypeVariants[0]; - $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); - $methodParameters = $methodVariant->getParameters(); - - 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; - } + $methodReturnType = $method->getNativeReturnType(); - $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(); - } + $realPrototype = $method->getPrototype(); - if ($prototypeParameter->isVariadic()) { - if (!$methodParameter->isVariadic()) { - $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 ($methodParameter->isVariadic()) { - $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->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 { + if ( + $realPrototype instanceof MethodPrototypeReflection + && $this->phpVersion->hasTentativeReturnTypes() + && $realPrototype->getTentativeReturnType() !== null + && !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()) + && count($prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName())->getAttributes('ReturnTypeWillChange')) === 0 + ) { + if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($realPrototype->getTentativeReturnType(), $method->getNativeReturnType(), true)) { $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()), + '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(), - $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 ($methodParameter->isOptional()) { - continue; + $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(); } - - $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(); } - $methodReturnType = $methodVariant->getNativeReturnType(); + $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, false)); - if (!$prototypeVariant instanceof FunctionVariantWithPhpDocs) { + if (!$prototypeVariant instanceof ExtendedFunctionVariant) { return $this->addErrors($messages, $node, $scope); } $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; + } + } + + if ( + $reportReturnType + && (is_bool($prototype->isBuiltin()) ? $prototype->isBuiltin() : $prototype->isBuiltin()->yes()) + ) { + $reportReturnType = !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()); + } + } + } - 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().', @@ -314,9 +253,12 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->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().', @@ -324,64 +266,136 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() - ))->nonIgnorable()->build(); + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.childReturnType') + ->build(); } } return $this->addErrors($messages, $node, $scope); } - private function isTypeCompatible(Type $methodParameterType, Type $prototypeParameterType, bool $supportsContravariance): bool + /** + * @param list $errors + * @return list + */ + private function addErrors( + array $errors, + InClassMethodNode $classMethod, + Scope $scope, + ): array { - if ($methodParameterType instanceof MixedType) { - return true; + if (count($errors) > 0) { + return $errors; } - if (!$supportsContravariance) { - if (TypeCombinator::containsNull($methodParameterType)) { - $prototypeParameterType = TypeCombinator::removeNull($prototypeParameterType); - } - $methodParameterType = TypeCombinator::removeNull($methodParameterType); - if ($methodParameterType->equals($prototypeParameterType)) { - return true; - } + if (!$this->checkPhpDocMethodSignatures) { + return $errors; + } + + return $this->methodSignatureRule->processNode($classMethod, $scope); + } - if ($methodParameterType instanceof IterableType) { - if ($prototypeParameterType instanceof ArrayType) { + private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method): bool + { + foreach ($method->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'returntypewillchange') { return true; } - if ($prototypeParameterType instanceof ObjectType && $prototypeParameterType->getClassName() === \Traversable::class) { + } + } + + 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 $methodParameterType->isSuperTypeOf($prototypeParameterType)->yes(); + return false; } /** - * @param RuleError[] $errors - * @return (string|RuleError)[] + * @return array{ExtendedMethodReflection, ClassReflection, bool}|null */ - private function addErrors( - array $errors, - InClassMethodNode $classMethod, - Scope $scope - ): array + private function findPrototype(ClassReflection $classReflection, string $methodName): ?array { - if (count($errors) > 0) { - return $errors; + foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasNativeMethod($methodName)) { + $method = $immediateInterface->getNativeMethod($methodName); + return [$method, $method->getDeclaringClass(), true]; + } } - if (!$this->checkPhpDocMethodSignatures) { - return $errors; + 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, + ]; + } + } } - return $this->methodSignatureRule->processNode($classMethod, $scope); + $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 98c5d27469..d580b0c2b9 100644 --- a/src/Rules/Methods/ReturnTypeRule.php +++ b/src/Rules/Methods/ReturnTypeRule.php @@ -5,21 +5,32 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\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 \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Return_> + * @implements Rule */ -class ReturnTypeRule implements \PHPStan\Rules\Rule +final class ReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - public function __construct(FunctionReturnTypeCheck $returnTypeCheck) + public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) { - $this->returnTypeCheck = $returnTypeCheck; } public function getNodeType(): string @@ -38,36 +49,82 @@ public function processNode(Node $node, Scope $scope): array } $method = $scope->getFunction(); - if (!($method instanceof MethodReflection)) { + if (!$method instanceof PhpMethodFromParserNodeReflection) { return []; } - $reflection = null; - if ($method->getDeclaringClass()->getNativeReflection()->hasMethod($method->getName())) { - $reflection = $method->getDeclaringClass()->getNativeReflection()->getMethod($method->getName()); + 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()); } - return $this->returnTypeCheck->checkReturnType( + $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( + '%s should never return but return statement found.', + $methodDescription, ), - $reflection !== null && $reflection->isGenerator() + $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 new file mode 100644 index 0000000000..373e41ddd9 --- /dev/null +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -0,0 +1,320 @@ +, ExtendedMethodReflection|null} + */ + public function check( + Scope $scope, + string $methodName, + $class, + ): array + { + $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)) { + if (!$scope->isInClass()) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Calling %s::%s() outside of class scope.', + $className, + $methodName, + ))->identifier(sprintf('outOfClass.%s', $lowercasedClassName))->build(), + ], + null, + ]; + } + $classType = $scope->resolveTypeByName($class); + } elseif ($lowercasedClassName === 'parent') { + if (!$scope->isInClass()) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Calling %s::%s() outside of class scope.', + $className, + $methodName, + ))->identifier(sprintf('outOfClass.parent'))->build(), + ], + null, + ]; + } + $currentClassReflection = $scope->getClassReflection(); + if ($currentClassReflection->getParentClass() === null) { + return [ + [ + RuleErrorBuilder::message(sprintf( + '%s::%s() calls parent::%s() but %s does not extend any class.', + $scope->getClassReflection()->getDisplayName(), + $scope->getFunctionName(), + $methodName, + $scope->getClassReflection()->getDisplayName(), + ))->identifier('class.noParent')->build(), + ], + null, + ]; + } + + if ($scope->getFunctionName() === null) { + throw new ShouldNotHappenException(); + } + + $classType = $scope->resolveTypeByName($class); + } else { + if (!$this->reflectionProvider->hasClass($className)) { + if ($scope->isInClassExists($className)) { + return [[], null]; + } + + $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 [ + [ + $errorBuilder->build(), + ], + null, + ]; + } + + $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); + } + + $classReflection = $classType->getClassReflection(); + if ($classReflection !== null && $classReflection->hasNativeMethod($methodName) && $lowercasedClassName !== 'static') { + $nativeMethodReflection = $classReflection->getNativeMethod($methodName); + if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { + $isAbstract = $nativeMethodReflection->isAbstract(); + if ($isAbstract instanceof TrinaryLogic) { + $isAbstract = $isAbstract->yes(); + } + } + } + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class), + sprintf('Call to static method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)), + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + $classType = $classTypeResult->getType(); + if ($classType instanceof ErrorType) { + return [$classTypeResult->getUnknownClassErrors(), null]; + } + } + + if ($classType instanceof GenericClassStringType) { + $classType = $classType->getGenericType(); + if (!$classType->isObject()->yes()) { + return [[], null]; + } + } elseif ($classType->isString()->yes()) { + return [[], null]; + } + + $typeForDescribe = $classType; + if ($classType instanceof StaticType) { + $typeForDescribe = $classType->getStaticObjectType(); + } + $classType = TypeCombinator::remove($classType, new StringType()); + + if (!$classType->canCallMethods()->yes()) { + return [ + array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Cannot call static method %s() on %s.', + $methodName, + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('staticMethod.nonObject')->build(), + ]), + null, + ]; + } + + if (!$classType->hasMethod($methodName)->yes()) { + if (!$this->reportMagicMethods) { + foreach ($classType->getObjectClassNames() as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->hasNativeMethod('__callStatic')) { + return [[], null]; + } + } + } + + return [ + array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Call to an undefined static method %s::%s().', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + $methodName, + ))->identifier('staticMethod.notFound')->build(), + ]), + null, + ]; + } + + $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() + || $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(), + ))->identifier('method.staticCall')->build(), + ]), + $method, + ]; + } + } + + if (!$scope->canCallMethod($method)) { + $errors = array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s %s() of class %s.', + $method->isPrivate() ? 'private' : 'protected', + $method->isStatic() ? 'static method' : 'method', + $method->getName(), + $method->getDeclaringClass()->getDisplayName(), + )) + ->identifier(sprintf('staticMethod.%s', $method->isPrivate() ? 'private' : 'protected')) + ->build(), + ]); + } + + if ($isAbstract) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Cannot call abstract%s method %s::%s().', + $method->isStatic() ? ' static' : '', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier(sprintf( + '%s.callToAbstract', + $method->isStatic() ? 'staticMethod' : 'method', + ))->build(), + ], + $method, + ]; + } + + $lowercasedMethodName = SprintfHelper::escapeFormatString(sprintf( + '%s %s', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()', + )); + + if ( + $this->checkFunctionNameCase + && $method->getName() !== $methodName + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s with incorrect case: %s', + $lowercasedMethodName, + $methodName, + ))->identifier('staticMethod.nameCase')->build(); + } + + return [$errors, $method]; + } + +} diff --git a/src/Rules/Methods/StaticMethodCallableRule.php b/src/Rules/Methods/StaticMethodCallableRule.php new file mode 100644 index 0000000000..815fdce793 --- /dev/null +++ b/src/Rules/Methods/StaticMethodCallableRule.php @@ -0,0 +1,66 @@ + + */ +final class StaticMethodCallableRule implements Rule +{ + + public function __construct(private StaticMethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return StaticMethodCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->identifier('callable.notSupported') + ->build(), + ]; + } + + $methodName = $node->getName(); + if (!$methodName instanceof Node\Identifier) { + return []; + } + + $methodNameName = $methodName->toString(); + + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getClass()); + if ($methodReflection === null) { + return $errors; + } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->hasNativeMethod($methodNameName)) { + return $errors; + } + + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName)) + ->identifier('callable.nonNativeMethod') + ->build(); + + return $errors; + } + +} diff --git a/src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php b/src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php deleted file mode 100644 index 547b06f3c2..0000000000 --- a/src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php +++ /dev/null @@ -1,137 +0,0 @@ - - */ -class MissingClosureNativeReturnTypehintRule implements Rule -{ - - private bool $checkObjectTypehint; - - public function __construct(bool $checkObjectTypehint) - { - $this->checkObjectTypehint = $checkObjectTypehint; - } - - public function getNodeType(): string - { - return ClosureReturnStatementsNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $closure = $node->getClosureExpr(); - if ($closure->returnType !== null) { - return []; - } - - $messagePattern = 'Anonymous function should have native return typehint "%s".'; - $statementResult = $node->getStatementResult(); - if ($statementResult->hasYield()) { - return [ - RuleErrorBuilder::message(sprintf($messagePattern, 'Generator'))->build(), - ]; - } - - $returnStatements = $node->getReturnStatements(); - if (count($returnStatements) === 0) { - return [ - RuleErrorBuilder::message(sprintf($messagePattern, 'void'))->build(), - ]; - } - - $returnTypes = []; - $voidReturnNodes = []; - $hasNull = false; - foreach ($returnStatements as $returnStatement) { - $returnNode = $returnStatement->getReturnNode(); - if ($returnNode->expr === null) { - $voidReturnNodes[] = $returnNode; - $hasNull = true; - continue; - } - - $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); - } - - if (count($returnTypes) === 0) { - return [ - RuleErrorBuilder::message(sprintf($messagePattern, 'void'))->build(), - ]; - } - - $messages = []; - foreach ($voidReturnNodes as $voidReturnStatement) { - $messages[] = RuleErrorBuilder::message('Mixing returning values with empty return statements - return null should be used here.') - ->line($voidReturnStatement->getLine()) - ->build(); - } - - $returnType = TypeCombinator::union(...$returnTypes); - if ( - $returnType instanceof MixedType - || $returnType instanceof NeverType - || $returnType instanceof IntersectionType - || $returnType instanceof NullType - ) { - return $messages; - } - - if (TypeCombinator::containsNull($returnType)) { - $hasNull = true; - $returnType = TypeCombinator::removeNull($returnType); - } - - if ( - $returnType instanceof UnionType - || $returnType instanceof ResourceType - ) { - return $messages; - } - - if (!$statementResult->isAlwaysTerminating()) { - $messages[] = RuleErrorBuilder::message('Anonymous function sometimes return something but return statement at the end is missing.')->build(); - return $messages; - } - - $returnType = TypeUtils::generalizeType($returnType); - $description = $returnType->describe(VerbosityLevel::typeOnly()); - if ($returnType instanceof ArrayType) { - $description = 'array'; - } - if ($hasNull) { - $description = '?' . $description; - } - - if ( - !$this->checkObjectTypehint - && $returnType instanceof ObjectWithoutClassType - ) { - return $messages; - } - - $messages[] = RuleErrorBuilder::message(sprintf($messagePattern, $description))->build(); - - return $messages; - } - -} diff --git a/src/Rules/Missing/MissingReturnRule.php b/src/Rules/Missing/MissingReturnRule.php index c95149b106..ef53fa16c8 100644 --- a/src/Rules/Missing/MissingReturnRule.php +++ b/src/Rules/Missing/MissingReturnRule.php @@ -2,37 +2,36 @@ namespace PHPStan\Rules\Missing; +use Generator; 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\TypeWithClassName; +use PHPStan\Type\NeverType; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function sprintf; +use function ucfirst; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ExecutionEndNode> + * @implements Rule */ -class MissingReturnRule implements Rule +final class MissingReturnRule implements Rule { - private bool $checkExplicitMixedMissingReturn; - - private bool $checkPhpDocMissingReturn; - public function __construct( - bool $checkExplicitMixedMissingReturn, - bool $checkPhpDocMissingReturn + private bool $checkExplicitMixedMissingReturn, + private bool $checkPhpDocMissingReturn, ) { - $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; - $this->checkPhpDocMissingReturn = $checkPhpDocMissingReturn; } public function getNodeType(): string @@ -52,39 +51,47 @@ public function processNode(Node $node, Scope $scope): array if ($anonymousFunctionReturnType !== null) { $returnType = $anonymousFunctionReturnType; $description = 'Anonymous function'; + if (!$node->hasNativeReturnTypehint()) { + 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()); } } else { - throw new \PHPStan\ShouldNotHappenException(); + 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(), + sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), + ) + ->line($node->getNode()->getStartLine()) + ->identifier('return.missing') + ->build(), ]; } } @@ -92,13 +99,32 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$node->hasNativeReturnTypehint() && !$this->checkPhpDocMissingReturn) { + if ( + !$node->hasNativeReturnTypehint() + && !$this->checkPhpDocMissingReturn + && TypeCombinator::containsNull($returnType) + ) { return []; } + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $errorBuilder = RuleErrorBuilder::message(sprintf('%s should always throw an exception or terminate script execution but doesn\'t do that.', $description))->line($node->getNode()->getStartLine()); + + if ($node->hasNativeReturnTypehint()) { + $errorBuilder->nonIgnorable(); + } + + $errorBuilder->identifier('return.never'); + + return [ + $errorBuilder->build(), + ]; + } + if ( $returnType instanceof MixedType && !$returnType instanceof TemplateMixedType + && !$node->hasNativeReturnTypehint() && ( !$returnType->isExplicitMixed() || !$this->checkExplicitMixedMissingReturn @@ -107,10 +133,18 @@ public function processNode(Node $node, Scope $scope): array return []; } + $errorBuilder = RuleErrorBuilder::message( + sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), + )->line($node->getNode()->getStartLine()); + + if ($node->hasNativeReturnTypehint()) { + $errorBuilder->nonIgnorable(); + } + + $errorBuilder->identifier('return.missing'); + return [ - RuleErrorBuilder::message( - sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())) - )->line($node->getNode()->getStartLine())->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 66336c8fec..f6910907e1 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -2,78 +2,91 @@ namespace PHPStan\Rules; -use PHPStan\Reflection\ReflectionProvider; +use Closure; +use Generator; +use Iterator; +use IteratorAggregate; +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; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; - -class MissingTypehintCheck +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; + +final class MissingTypehintCheck { - public const TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP = "Consider adding something like %s to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %%configurationFile%%."; - - 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, - \Iterator::class, - \IteratorAggregate::class, - \Generator::class, + Traversable::class, + Iterator::class, + IteratorAggregate::class, + Generator::class, ]; - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkMissingIterableValueType; - - private bool $checkGenericClassInNonGenericObjectType; - + /** + * @param string[] $skipCheckGenericClasses + */ public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkMissingIterableValueType, - bool $checkGenericClassInNonGenericObjectType + private bool $checkMissingCallableSignature, + private array $skipCheckGenericClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkMissingIterableValueType = $checkMissingIterableValueType; - $this->checkGenericClassInNonGenericObjectType = $checkGenericClassInNonGenericObjectType; } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Type[] + * @return Type[] */ 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) { return $type; } + 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; } - return $type; + if ($type instanceof IntersectionType) { + if ($type->isList()->yes()) { + return $traverse($iterableValue); + } + + return $type; + } } return $traverse($type); }); @@ -82,18 +95,14 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return array + * @return array */ public function getNonGenericObjectTypesWithGenericClass(Type $type): array { - if (!$this->checkGenericClassInNonGenericObjectType) { - return []; - } - $objectTypes = []; - TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$objectTypes): Type { - if ($type instanceof GenericObjectType) { + TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$objectTypes): Type { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { + $traverse($type); return $type; } if ($type instanceof TemplateType) { @@ -108,6 +117,9 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array // checked by getIterableTypesWithMissingValueTypehint() already return $type; } + if (in_array($classReflection->getName(), $this->skipCheckGenericClasses, true)) { + return $type; + } if ($classReflection->isTrait()) { return $type; } @@ -117,19 +129,56 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array $resolvedType = TemplateTypeHelper::resolveToBounds($type); if (!$resolvedType instanceof ObjectType) { - throw new \PHPStan\ShouldNotHappenException(); + 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; } - $traverse($type); - return $type; + + return $traverse($type); }); return $objectTypes; } + /** + * @return Type[] + */ + public function getCallablesWithMissingSignature(Type $type): array + { + if (!$this->checkMissingCallableSignature) { + return []; + } + + $result = []; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { + if ( + ($type instanceof CallableType && $type->isCommonCallable()) + || ($type instanceof ClosureType && $type->isCommonCallable()) + || ($type instanceof ObjectType && $type->getClassName() === Closure::class) + ) { + $result[] = $type; + } + return $traverse($type); + }); + + return $result; + } + } 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 479e4152a7..b167becd52 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -6,37 +6,34 @@ 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\RuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function count; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\GroupUse> + * @implements Rule */ -class ExistingNamesInGroupUseRule implements \PHPStan\Rules\Rule +final class ExistingNamesInGroupUseRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\GroupUse::class; + return Node\Stmt\GroupUse::class; } public function processNode(Node $node, Scope $scope): array @@ -46,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 @@ -58,9 +55,9 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($error === null) { @@ -73,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))->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))->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) { @@ -99,26 +112,29 @@ private function checkFunction(Node\Name $name): ?RuleError return RuleErrorBuilder::message(sprintf( 'Function %s used with incorrect case: %s.', $realName, - $usedName - ))->line($name->getLine())->build(); + $usedName, + )) + ->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) { return $errors[0]; } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Rules/Namespaces/ExistingNamesInUseRule.php b/src/Rules/Namespaces/ExistingNamesInUseRule.php index 1a125bceaf..daf1ee2ce1 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -5,48 +5,45 @@ 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\RuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function array_map; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Use_> + * @implements Rule */ -class ExistingNamesInUseRule implements \PHPStan\Rules\Rule +final class ExistingNamesInUseRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private bool $checkFunctionNameCase, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\Use_::class; + return Node\Stmt\Use_::class; } public function processNode(Node $node, Scope $scope): array { if ($node->type === Node\Stmt\Use_::TYPE_UNKNOWN) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } foreach ($node->uses as $use) { if ($use->type !== Node\Stmt\Use_::TYPE_UNKNOWN) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } @@ -58,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 \PhpParser\Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @param Node\UseItem[] $uses + * @return list */ private function checkConstants(array $uses): array { @@ -73,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())->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 \PhpParser\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())->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(); @@ -100,8 +113,11 @@ private function checkFunctions(array $uses): array $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s used with incorrect case: %s.', $realName, - $usedName - ))->line($use->name->getLine())->build(); + $usedName, + )) + ->line($use->name->getStartLine()) + ->identifier('function.nameCase') + ->build(); } } } @@ -110,15 +126,15 @@ private function checkFunctions(array $uses): array } /** - * @param \PhpParser\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 function (\PhpParser\Node\Stmt\UseUse $use): ClassNameNodePair { - return 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 new file mode 100644 index 0000000000..a4424b69ac --- /dev/null +++ b/src/Rules/NullsafeCheck.php @@ -0,0 +1,58 @@ +containsNullSafe($expr->var); + } + + if ($expr instanceof Expr\PropertyFetch) { + return $this->containsNullSafe($expr->var); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->containsNullSafe($expr->class); + } + + if ($expr instanceof Expr\MethodCall) { + return $this->containsNullSafe($expr->var); + } + + if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { + return $this->containsNullSafe($expr->class); + } + + if ($expr instanceof Expr\List_) { + foreach ($expr->items as $item) { + if ($item === null) { + continue; + } + + if ($item->key !== null && $this->containsNullSafe($item->key)) { + return true; + } + + if ($this->containsNullSafe($item->value)) { + return true; + } + } + } + + return false; + } + +} diff --git a/src/Rules/Operators/InvalidAssignVarRule.php b/src/Rules/Operators/InvalidAssignVarRule.php new file mode 100644 index 0000000000..a5ae2ac291 --- /dev/null +++ b/src/Rules/Operators/InvalidAssignVarRule.php @@ -0,0 +1,106 @@ + + */ +final class InvalidAssignVarRule implements Rule +{ + + public function __construct(private NullsafeCheck $nullsafeCheck) + { + } + + public function getNodeType(): string + { + return Expr::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + !$node instanceof Assign + && !$node instanceof AssignOp + && !$node instanceof AssignRef + ) { + return []; + } + + if ($this->nullsafeCheck->containsNullSafe($node->var)) { + return [ + 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.') + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(), + ]; + } + + if ($this->containsNonAssignableExpression($node->var)) { + return [ + 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) { + return false; + } + + if ($expr instanceof Expr\PropertyFetch) { + return false; + } + + if ($expr instanceof Expr\ArrayDimFetch) { + return false; + } + + if ($expr instanceof Expr\StaticPropertyFetch) { + return false; + } + + if ($expr instanceof Expr\List_) { + foreach ($expr->items as $item) { + if ($item === null) { + continue; + } + if (!$this->containsNonAssignableExpression($item->value)) { + continue; + } + + return true; + } + + return false; + } + + return true; + } + +} diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 9cabaec6be..e44b2178d5 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -5,29 +5,30 @@ use PhpParser\Node; 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; +use function sprintf; +use function strlen; +use function substr; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class InvalidBinaryOperationRule implements \PHPStan\Rules\Rule +final class InvalidBinaryOperationRule implements Rule { - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - \PhpParser\PrettyPrinter\Standard $printer, - RuleLevelHelper $ruleLevelHelper + private ExprPrinter $exprPrinter, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->printer = $printer; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -35,7 +36,7 @@ public function getNodeType(): string return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( !$node instanceof Node\Expr\BinaryOp @@ -44,78 +45,79 @@ public function processNode(\PhpParser\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 function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - }; - } else { - $callback = static function (Type $type): bool { - return !$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 \PHPStan\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 83c4ac30ad..8dc06429e5 100644 --- a/src/Rules/Operators/InvalidComparisonOperationRule.php +++ b/src/Rules/Operators/InvalidComparisonOperationRule.php @@ -4,27 +4,30 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +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\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 \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp> + * @implements Rule */ -class InvalidComparisonOperationRule implements \PHPStan\Rules\Rule +final class InvalidComparisonOperationRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -46,21 +49,54 @@ 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->isObjectType($scope, $node->right) || $this->isArrayType($scope, $node->right) + $this->isPossiblyNullableObjectType($scope, $node->right) || $this->isPossiblyNullableArrayType($scope, $node->right) )) || ($this->isNumberType($scope, $node->right) && ( - $this->isObjectType($scope, $node->left) || $this->isArrayType($scope, $node->left) + $this->isPossiblyNullableObjectType($scope, $node->left) || $this->isPossiblyNullableArrayType($scope, $node->left) )) ) { + 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(), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + )) + ->line($node->left->getStartLine()) + ->identifier(sprintf('%s.invalid', $nodeType)) + ->build(), ]; } @@ -70,9 +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 function (Type $type) use ($acceptedType): bool { - return $acceptedType->accepts($type, true)->yes(); - }; + $onlyNumber = static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(); $type = $this->ruleLevelHelper->findTypeToCheck($scope, $expr, '', $onlyNumber)->getType(); @@ -83,10 +117,11 @@ 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 isObjectType(Scope $scope, Node\Expr $expr): bool + private function isPossiblyNullableObjectType(Scope $scope, Node\Expr $expr): bool { $acceptedType = new ObjectWithoutClassType(); @@ -94,34 +129,38 @@ private function isObjectType(Scope $scope, Node\Expr $expr): bool $scope, $expr, '', - static function (Type $type) use ($acceptedType): bool { - return $acceptedType->isSuperTypeOf($type)->yes(); - } + static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(), )->getType(); if ($type instanceof ErrorType) { return false; } + if (TypeCombinator::containsNull($type) && !$type->isNull()->yes()) { + $type = TypeCombinator::removeNull($type); + } + $isSuperType = $acceptedType->isSuperTypeOf($type); - if ($type instanceof \PHPStan\Type\BenevolentUnionType) { + if ($type instanceof BenevolentUnionType) { return !$isSuperType->no(); } return $isSuperType->yes(); } - private function isArrayType(Scope $scope, Node\Expr $expr): bool + private function isPossiblyNullableArrayType(Scope $scope, Node\Expr $expr): bool { $type = $this->ruleLevelHelper->findTypeToCheck( $scope, $expr, '', - static function (Type $type): bool { - return $type->isArray()->yes(); - } + static fn (Type $type): bool => $type->isArray()->yes(), )->getType(); + if (TypeCombinator::containsNull($type) && !$type->isNull()->yes()) { + $type = TypeCombinator::removeNull($type); + } + return !($type instanceof ErrorType) && $type->isArray()->yes(); } diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index 1bbc260fa5..8283936e73 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -2,74 +2,111 @@ namespace PHPStan\Rules\Operators; +use PhpParser\Node; +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 \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class InvalidIncDecOperationRule implements \PHPStan\Rules\Rule +final class InvalidIncDecOperationRule implements Rule { - private bool $checkThisOnly; - - public function __construct(bool $checkThisOnly) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) { - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, \PHPStan\Analyser\Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( - !$node instanceof \PhpParser\Node\Expr\PreInc - && !$node instanceof \PhpParser\Node\Expr\PostInc - && !$node instanceof \PhpParser\Node\Expr\PreDec - && !$node instanceof \PhpParser\Node\Expr\PostDec + !$node instanceof Node\Expr\PreInc + && !$node instanceof Node\Expr\PostInc + && !$node instanceof Node\Expr\PreDec + && !$node instanceof Node\Expr\PostDec ) { return []; } - $operatorString = $node instanceof \PhpParser\Node\Expr\PreInc || $node instanceof \PhpParser\Node\Expr\PostInc ? '++' : '--'; + 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 ( - !$node->var instanceof \PhpParser\Node\Expr\Variable - && !$node->var instanceof \PhpParser\Node\Expr\ArrayDimFetch - && !$node->var instanceof \PhpParser\Node\Expr\PropertyFetch - && !$node->var instanceof \PhpParser\Node\Expr\StaticPropertyFetch + !$node->var instanceof Node\Expr\Variable + && !$node->var instanceof Node\Expr\ArrayDimFetch + && !$node->var instanceof Node\Expr\PropertyFetch + && !$node->var instanceof Node\Expr\StaticPropertyFetch ) { return [ RuleErrorBuilder::message(sprintf( 'Cannot use %s on a non-variable.', - $operatorString - ))->line($node->var->getLine())->build(), + $operatorString, + )) + ->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 5ddeda862a..6600cce4ad 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -2,42 +2,94 @@ 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 \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class InvalidUnaryOperationRule implements \PHPStan\Rules\Rule +final class InvalidUnaryOperationRule implements Rule { + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( - !$node instanceof \PhpParser\Node\Expr\UnaryPlus - && !$node instanceof \PhpParser\Node\Expr\UnaryMinus + !$node instanceof Node\Expr\UnaryPlus + && !$node instanceof Node\Expr\UnaryMinus + && !$node instanceof Node\Expr\BitwiseNot ) { return []; } - if ($scope->getType($node) instanceof ErrorType) { - return [ - RuleErrorBuilder::message(sprintf( - 'Unary operation "%s" on %s results in an error.', - $node instanceof \PhpParser\Node\Expr\UnaryPlus ? '+' : '-', - $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 new file mode 100644 index 0000000000..0008644c1a --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -0,0 +1,138 @@ + + */ +final class IncompatibleClassConstantPhpDocTypeRule implements Rule +{ + + public function __construct( + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $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(), $nativeType, $constantName)); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, ?Type $nativeType, string $constantName): array + { + $constantReflection = $classReflection->getConstant($constantName); + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { + return []; + } + + $errors = []; + if ( + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s contains unresolvable type.', + $constantReflection->getDeclaringClass()->getName(), + $constantName, + ))->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 native type %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $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 native type %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); + } + } + + $className = SprintfHelper::escapeFormatString($constantReflection->getDeclaringClass()->getDisplayName()); + $escapedConstantName = SprintfHelper::escapeFormatString($constantName); + + return array_merge($errors, $this->genericObjectTypeCheck->check( + $phpDocType, + sprintf( + 'PHPDoc tag @var for constant %s::%s contains generic type %%s but %%s %%s is not generic.', + $className, + $escapedConstantName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of %%s %%s: %%s', + $className, + $escapedConstantName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $className, + $escapedConstantName, + ), + sprintf( + 'Type %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is not subtype of template type %%s of %%s %%s.', + $className, + $escapedConstantName, + ), + 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 48015e4187..47b3a78248 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -5,52 +5,44 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ArrayType; -use PHPStan\Type\ErrorType; +use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\TemplateTypeHelper; -use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; +use function is_string; +use function trim; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\FunctionLike> + * @implements Rule */ -class IncompatiblePhpDocTypeRule implements \PHPStan\Rules\Rule +final class IncompatiblePhpDocTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericObjectTypeCheck $genericObjectTypeCheck + private FileTypeMapper $fileTypeMapper, + private IncompatiblePhpDocTypeCheck $check, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; } public function getNodeType(): string { - return \PhpParser\Node\FunctionLike::class; + return Node\FunctionLike::class; } 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_) { $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); + } else { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; } $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( @@ -58,142 +50,58 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $functionName, - $docComment->getText() + $docComment->getText(), ); - $nativeParameterTypes = $this->getNativeParameterTypes($node, $scope); - $nativeReturnType = $this->getNativeReturnType($node, $scope); - - $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 ( - $phpDocParamType instanceof ErrorType - || ($phpDocParamType instanceof NeverType && !$phpDocParamType->isExplicit()) - ) { - $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(TemplateTypeHelper::resolveToBounds($phpDocParamType)); - - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $phpDocParamType, - sprintf( - 'PHPDoc tag @param for parameter $%s contains generic type %%s but class %%s is not generic.', - $parameterName - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of class %%s: %%s', - $parameterName - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but class %%s supports only %%d: %%s', - $parameterName - ), - sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of class %%s.', - $parameterName - ) - )); - - 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()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()) - ))->build(); - } - } - } - - if ($resolvedPhpDoc->getReturnTag() !== null) { - $phpDocReturnType = TemplateTypeHelper::resolveToBounds($resolvedPhpDoc->getReturnTag()->getType()); - - if ( - $phpDocReturnType instanceof ErrorType - || ($phpDocReturnType instanceof NeverType && !$phpDocReturnType->isExplicit()) - ) { - $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 class %s is not generic.', - 'Generic type %s in PHPDoc tag @return does not specify all template types of class %s: %s', - 'Generic type %s in PHPDoc tag @return specifies %d template types, but class %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @return is not subtype of template type %s of class %s.' - )); - 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()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @return with type %s is not subtype of native type %s.', - $phpDocReturnType->describe(VerbosityLevel::typeOnly()), - $nativeReturnType->describe(VerbosityLevel::typeOnly()) - ))->build(); - } - } - } - - return $errors; + return $this->check->check( + $scope, + $node, + $resolvedPhpDoc, + $functionName, + $this->getNativeParameterTypes($node, $scope), + $this->getByRefParameters($node), + $this->getNativeReturnType($node, $scope), + ); } /** - * @param Node\FunctionLike $node - * @param Scope $scope - * @return Type[] + * @return array */ - private function getNativeParameterTypes(\PhpParser\Node\FunctionLike $node, Scope $scope): array + private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): array { $nativeParameterTypes = []; foreach ($node->getParams() as $parameter) { $isNullable = $scope->isParameterValueNullable($parameter); if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nativeParameterTypes[$parameter->var->name] = $scope->getFunctionType( $parameter->type, $isNullable, - false + false, ); } return $nativeParameterTypes; } - private function getNativeReturnType(\PhpParser\Node\FunctionLike $node, Scope $scope): Type + /** + * @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 d69e04ee16..a202f67bcd 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -4,101 +4,147 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ErrorType; -use PHPStan\Type\NeverType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\PropertyProperty> + * @implements Rule */ -class IncompatiblePropertyPhpDocTypeRule implements Rule +final class IncompatiblePropertyPhpDocTypeRule implements Rule { - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - public function __construct(GenericObjectTypeCheck $genericObjectTypeCheck) + public function __construct( + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, + ) { - $this->genericObjectTypeCheck = $genericObjectTypeCheck; } public function getNodeType(): string { - return Node\Stmt\PropertyProperty::class; + return ClassPropertyNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + $phpDocType = $node->getPhpDocType(); + if ($phpDocType === null) { + return []; } - $propertyName = $node->name->toString(); - $propertyReflection = $scope->getClassReflection()->getNativeProperty($propertyName); + $propertyName = $node->getName(); - if (!$propertyReflection->hasPhpDoc()) { - return []; + $description = 'PHPDoc tag @var'; + if ($node->isPromoted()) { + $description = 'PHPDoc type'; } - $phpDocType = $propertyReflection->getPhpDocType(); + $classReflection = $node->getClassReflection(); $messages = []; if ( - $phpDocType instanceof ErrorType - || ($phpDocType instanceof NeverType && !$phpDocType->isExplicit()) + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) ) { $messages[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for property %s::$%s contains unresolvable type.', - $propertyReflection->getDeclaringClass()->getName(), - $propertyName - ))->build(); + '%s for property %s::$%s contains unresolvable type.', + $description, + $classReflection->getDisplayName(), + $propertyName, + ))->identifier('property.unresolvableType')->build(); } - $nativeType = $propertyReflection->getNativeType(); - $isSuperType = $nativeType->isSuperTypeOf($phpDocType); - if ($isSuperType->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for property %s::$%s with type %s is incompatible with native type %s.', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName, - $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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(); + } + } - } elseif ($isSuperType->maybe()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for property %s::$%s with type %s is not subtype of native type %s.', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName, - $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $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( - 'PHPDoc tag @var for property %s::$%s contains generic type %%s but class %%s is not generic.', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + '%s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', + $description, + $className, + $escapedPropertyName, + ), + sprintf( + 'Generic type %%s in %s for property %s::$%s does not specify all template types of %%s %%s: %%s', + $description, + $className, + $escapedPropertyName, ), sprintf( - 'Generic type %%s in PHPDoc tag @var for property %s::$%s does not specify all template types of class %%s: %%s', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + 'Generic type %%s in %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $description, + $className, + $escapedPropertyName, ), sprintf( - 'Generic type %%s in PHPDoc tag @var for property %s::$%s specifies %%d template types, but class %%s supports only %%d: %%s', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + 'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', + $description, + $className, + $escapedPropertyName, ), sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @var for property %s::$%s is not subtype of template type %%s of class %%s.', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName - ) + '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 830496aee5..c9e27ca74a 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -3,59 +3,90 @@ 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; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function in_array; +use function sprintf; +use function str_starts_with; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node> + * @implements Rule */ -class InvalidPHPStanDocTagRule implements \PHPStan\Rules\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', ]; - private Lexer $phpDocLexer; - - private PhpDocParser $phpDocParser; - - public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + ) { - $this->phpDocLexer = $phpDocLexer; - $this->phpDocParser = $phpDocParser; } public function getNodeType(): string { - return \PhpParser\Node::class; + return 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) { @@ -67,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; @@ -75,8 +106,10 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Unknown PHPDoc tag: %s', - $phpDocTag->name - ))->build(); + $phpDocTag->name, + )) + ->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 3b0260a001..5e99af64f5 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -3,46 +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 str_starts_with; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node> + * @implements Rule */ -class InvalidPhpDocTagValueRule implements \PHPStan\Rules\Rule +final class InvalidPhpDocTagValueRule implements Rule { - private Lexer $phpDocLexer; - - private PhpDocParser $phpDocParser; - - public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + ) { - $this->phpDocLexer = $phpDocLexer; - $this->phpDocParser = $phpDocParser; } public function getNodeType(): string { - return \PhpParser\Node::class; + return 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 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) { @@ -55,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; } @@ -67,8 +88,10 @@ public function processNode(Node $node, Scope $scope): array 'PHPDoc tag %s has invalid value (%s): %s', $phpDocTag->name, $phpDocTag->value->value, - $phpDocTag->value->exception->getMessage() - ))->build(); + $phpDocTag->value->exception->getMessage(), + )) + ->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 ef6fa02bc3..7dc718889e 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -4,70 +4,53 @@ use PhpParser\Node; 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; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\NeverType; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function array_merge; +use function is_string; use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node> + * @implements Rule */ -class InvalidPhpDocVarTagTypeRule implements Rule +final class InvalidPhpDocVarTagTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private MissingTypehintCheck $missingTypehintCheck; - - private bool $checkClassCaseSensitivity; - - private bool $checkMissingVarTagTypehint; - public function __construct( - FileTypeMapper $fileTypeMapper, - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - MissingTypehintCheck $missingTypehintCheck, - bool $checkClassCaseSensitivity, - bool $checkMissingVarTagTypehint + private FileTypeMapper $fileTypeMapper, + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private MissingTypehintCheck $missingTypehintCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private bool $checkClassCaseSensitivity, + private bool $checkMissingVarTagTypehint, + private bool $discoveringSymbolsTip, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->missingTypehintCheck = $missingTypehintCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkMissingVarTagTypehint = $checkMissingVarTagTypehint; } public function getNodeType(): string { - return \PhpParser\Node::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array { if ( - !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - && !$node instanceof Node\Stmt\Static_ + $node instanceof Node\Stmt\Property + || $node instanceof Node\Stmt\ClassConst + || $node instanceof Node\Stmt\Const_ ) { return []; } @@ -83,7 +66,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $docComment->getText() + $docComment->getText(), ); $errors = []; @@ -94,10 +77,12 @@ public function processNode(Node $node, Scope $scope): array $identifier .= sprintf(' for variable $%s', $name); } if ( - $varTagType instanceof ErrorType - || ($varTagType instanceof NeverType && !$varTagType->isExplicit()) + $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; } @@ -107,55 +92,73 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( '%s has no value type specified in iterable type %s.', $identifier, - $iterableTypeDescription - ))->tip(sprintf(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, $iterableTypeDescription))->build(); + $iterableTypeDescription, + )) + ->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(); } } + $escapedIdentifier = SprintfHelper::escapeFormatString($identifier); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $varTagType, - sprintf('%s contains generic type %%s but class %%s is not generic.', $identifier), - sprintf('Generic type %%s in %s does not specify all template types of class %%s: %%s', $identifier), - sprintf('Generic type %%s in %s specifies %%d template types, but class %%s supports only %%d: %%s', $identifier), - sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of class %%s.', $identifier) + sprintf('%s contains generic type %%s but %%s %%s is not generic.', $escapedIdentifier), + sprintf('Generic type %%s in %s does not specify all template types of %%s %%s: %%s', $escapedIdentifier), + sprintf('Generic type %%s in %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedIdentifier), + sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of %%s %%s.', $escapedIdentifier), + 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)) { if ($this->reflectionProvider->getClass($referencedClass)->isTrait()) { $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s has invalid type %%s.', $identifier), - $referencedClass - ))->build(); + $referencedClass, + ))->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 - ))->build(); - } + $referencedClass, + )) + ->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 function (string $class) use ($node): ClassNameNodePair { - return 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 d3778f4c93..33a2e120c3 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -3,43 +3,53 @@ 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 \PHPStan\Rules\Rule<\PhpParser\Node\FunctionLike> + * @implements Rule */ -class InvalidThrowsPhpDocValueRule implements \PHPStan\Rules\Rule +final class InvalidThrowsPhpDocValueRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - public function __construct(FileTypeMapper $fileTypeMapper) + public function __construct(private FileTypeMapper $fileTypeMapper) { - $this->fileTypeMapper = $fileTypeMapper; } public function getNodeType(): string { - return \PhpParser\Node\FunctionLike::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array { + 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 []; + } + $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_) { - $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); + if ($scope->getFunction() !== null) { + $functionName = $scope->getFunction()->getName(); } $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( @@ -47,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $functionName, - $docComment->getText() + $docComment->getText(), ); if ($resolvedPhpDoc->getThrowsTag() === null) { @@ -55,21 +65,48 @@ 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 []; } return [ RuleErrorBuilder::message(sprintf( 'PHPDoc tag @throws with type %s is not subtype of Throwable', - $phpDocThrowsType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $phpDocThrowsType->describe(VerbosityLevel::typeOnly()), + ))->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..e34e2c36cc --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -0,0 +1,101 @@ + $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(); + $classNames = $type->getObjectClassNames(); + if (count($classNames) === 0) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireExtends.nonObject') + ->build(); + continue; + } + + sort($classNames); + $referencedClassReflections = array_map(static fn ($reflection) => [$reflection, $reflection->getName()], $type->getObjectClassReflections()); + $referencedClassReflectionsMap = array_column($referencedClassReflections, 0, 1); + foreach ($classNames as $class) { + $referencedClassReflection = $referencedClassReflectionsMap[$class] ?? null; + 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->isInterface()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain an interface %s, expected a class.', $class)) + ->tip('If you meant an interface, use @phpstan-require-implements instead.') + ->identifier('requireExtends.interface') + ->build(); + } elseif (!$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..f8eed38f4f --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -0,0 +1,98 @@ + + */ +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(); + $classNames = $type->getObjectClassNames(); + if (count($classNames) === 0) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireImplements.nonObject') + ->build(); + continue; + } + + $referencedClassReflections = array_map(static fn ($reflection) => [$reflection, $reflection->getName()], $type->getObjectClassReflections()); + $referencedClassReflectionsMap = array_column($referencedClassReflections, 0, 1); + foreach ($classNames as $class) { + $referencedClassReflection = $referencedClassReflectionsMap[$class] ?? null; + 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 new file mode 100644 index 0000000000..25b485dfad --- /dev/null +++ b/src/Rules/PhpDoc/UnresolvableTypeHelper.php @@ -0,0 +1,32 @@ +isExplicit()) { + $containsUnresolvable = true; + return $type; + } + + return $traverse($type); + }); + + return $containsUnresolvable; + } + +} 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 7e2a673789..72e61ffdb6 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -2,210 +2,289 @@ namespace PHPStan\Rules\PhpDoc; +use PhpParser\Comment\Doc; 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\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; +use function array_keys; +use function array_map; +use function array_merge; +use function count; +use function implode; +use function in_array; +use function is_int; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node> + * @implements Rule */ -class WrongVariableNameInVarTagRule implements Rule +final class WrongVariableNameInVarTagRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - public function __construct(FileTypeMapper $fileTypeMapper) + public function __construct( + private FileTypeMapper $fileTypeMapper, + private VarTagTypeRuleHelper $varTagTypeRuleHelper, + ) { - $this->fileTypeMapper = $fileTypeMapper; } public function getNodeType(): string { - return \PhpParser\Node::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array { if ( - !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - && !$node instanceof Node\Stmt\Static_ - && !$node instanceof Node\Stmt\Echo_ - && !$node instanceof Node\Stmt\Return_ - && !$node instanceof Node\Stmt\Expression - && !$node instanceof Node\Stmt\Throw_ - && !$node instanceof Node\Stmt\If_ - && !$node instanceof Node\Stmt\While_ - && !$node instanceof Node\Stmt\Switch_ - && !$node instanceof Node\Stmt\Nop + $node instanceof Node\Stmt\Property + || $node instanceof Node\Stmt\ClassConst + || $node instanceof Node\Stmt\Const_ + || ($node instanceof VirtualNode && !$node instanceof InFunctionNode && !$node instanceof InClassMethodNode && !$node instanceof InClassNode) ) { return []; } - $docComment = $node->getDocComment(); - if ($docComment === null) { - return []; - } - + $varTags = []; $function = $scope->getFunction(); + foreach ($node->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { + $varTags[$key] = $varTag; + } + } - $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 = $resolvedPhpDoc->getVarTags(); if (count($varTags) === 0) { return []; } - if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef) { - return $this->processAssign($scope, $node->var, $varTags); - } if ($node instanceof Node\Stmt\Foreach_) { - return $this->processForeach($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); } + if ($node instanceof Node\Stmt\Global_) { + return $this->processGlobal($scope, $node, $varTags); + } + + if ($node instanceof InClassNode || $node instanceof InClassMethodNode || $node instanceof InFunctionNode) { + $description = 'a function'; + $originalNode = $node->getOriginalNode(); + if ($originalNode instanceof Node\Stmt\Interface_) { + $description = 'an interface'; + } elseif ($originalNode instanceof Node\Stmt\Class_) { + $description = 'a class'; + } elseif ($originalNode instanceof Node\Stmt\Enum_) { + $description = 'an enum'; + } elseif ($originalNode instanceof Node\Stmt\Trait_) { + throw new ShouldNotHappenException(); + } elseif ($originalNode instanceof Node\Stmt\ClassMethod) { + $description = 'a method'; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var above %s has no effect.', + $description, + ))->identifier('varTag.misplaced')->build(), + ]; + } + return $this->processStmt($scope, $varTags, null); } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr $var - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return 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 { - if ($var instanceof Node\Expr\Variable && is_string($var->name)) { - if (count($varTags) === 1) { - $key = key($varTags); - if (is_int($key)) { - return []; + $errors = []; + $hasMultipleMessage = false; + $assignedVariables = $this->getAssignedVariables($var); + foreach (array_keys($varTags) as $key) { + if (is_int($key)) { + if (count($varTags) !== 1) { + if (!$hasMultipleMessage) { + $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.') + ->identifier('varTag.multipleTags') + ->build(); + $hasMultipleMessage = true; + } + } elseif (count($assignedVariables) !== 1) { + $errors[] = RuleErrorBuilder::message( + 'PHPDoc tag @var above assignment does not specify variable name.', + )->identifier('varTag.noVariable')->build(); } + continue; + } - if ($key !== $var->name) { - if (!$scope->hasVariableType($key)->no()) { - return []; - } + if (!$scope->hasVariableType($key)->no()) { + continue; + } + + if (in_array($key, $assignedVariables, true)) { + continue; + } + + if (count($assignedVariables) === 1 && count($varTags) === 1) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Variable $%s in PHPDoc tag @var does not match assigned variable $%s.', + $key, + $assignedVariables[0], + ))->identifier('varTag.differentVariable')->build(); + } else { + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key)) + ->identifier('varTag.variableNotFound') + ->build(); + } + } - return [ - RuleErrorBuilder::message(sprintf( - 'Variable $%s in PHPDoc tag @var does not match assigned variable $%s.', - $key, - $var->name - ))->build(), - ]; + if (count($errors) === 0) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var, $expr, $varTags, $assignedVariables) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return string[] + */ + private function getAssignedVariables(Expr $expr): array + { + if ($expr instanceof Expr\Variable) { + if (is_string($expr->name)) { + return [$expr->name]; + } + + return []; + } + + if ($expr instanceof Expr\List_) { + $names = []; + foreach ($expr->items as $item) { + if ($item === null) { + continue; } - return []; + $names = array_merge($names, $this->getAssignedVariables($item->value)); } - return [ - RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.')->build(), - ]; + return $names; } return []; } /** - * @param \PhpParser\Node\Expr|null $keyVar - * @param \PhpParser\Node\Expr $valueVar - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return list */ - private function processForeach(?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 ($keyVar instanceof Node\Expr\Variable && is_string($keyVar->name)) { - $variableNames[$keyVar->name] = true; - } - if ($valueVar instanceof Node\Expr\Variable && is_string($valueVar->name)) { - $variableNames[$valueVar->name] = true; + if ($iterateeExpr instanceof Node\Expr\Variable && is_string($iterateeExpr->name)) { + $variableNames[] = $iterateeExpr->name; } - if ($valueVar instanceof Node\Expr\Array_ || $valueVar instanceof Node\Expr\List_) { - $variableNames = $this->getVariablesFromList($variableNames, $valueVar->items); + if ($keyVar instanceof Node\Expr\Variable && is_string($keyVar->name)) { + $variableNames[] = $keyVar->name; } + $variableNames = array_merge($variableNames, $this->getAssignedVariables($valueVar)); $errors = []; foreach (array_keys($varTags) as $name) { if (is_int($name)) { + if (count($variableNames) === 1) { + continue; + } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above foreach loop does not specify variable name.' - )->build(); + 'PHPDoc tag @var above foreach loop does not specify variable name.', + )->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 variable in the foreach loop: %s', $name, - implode(', ', array_map(static function (string $name): string { - return sprintf('$%s', $name); - }, array_keys($variableNames))) - ))->build(); + implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), $variableNames)), + ))->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 array $variableNames - * @param (\PhpParser\Node\Expr\ArrayItem|null)[] $items - * @return array + * @param VarTag[] $varTags + * @return list */ - private function getVariablesFromList(array $variableNames, array $items): array + private function processExpression(Scope $scope, Expr $expr, array $varTags): array { - foreach ($items as $item) { - if ($item === null) { - continue; - } - - $value = $item->value; - - if ($value instanceof Node\Expr\Variable && is_string($value->name)) { - $variableNames[$value->name] = true; - continue; - } - - if (!($value instanceof Node\Expr\Array_) && !($value instanceof Node\Expr\List_)) { - continue; - } - - $variableNames = $this->getVariablesFromList($variableNames, $value->items); + if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { + return $this->processAssign($scope, $expr->var, $expr->expr, $varTags); } - return $variableNames; + return $this->processStmt($scope, $varTags, null); } /** - * @param \PhpParser\Node\Stmt\StaticVar[] $vars - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param Node\Stmt\StaticVar[] $vars + * @param VarTag[] $varTags + * @return list */ - private function processStatic(array $vars, array $varTags): array + private function processStatic(Scope $scope, array $vars, array $varTags): array { $variableNames = []; foreach ($vars as $var) { @@ -213,7 +292,7 @@ private function processStatic(array $vars, array $varTags): array continue; } - $variableNames[$var->var->name] = true; + $variableNames[] = $var->var->name; } $errors = []; @@ -224,47 +303,37 @@ private function processStatic(array $vars, array $varTags): array } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above multiple static variables does not specify variable name.' - )->build(); + 'PHPDoc tag @var above multiple static variables does not specify variable name.', + )->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 function (string $name): string { - return 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 \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr $expr - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] - */ - private function processExpression(Scope $scope, Expr $expr, array $varTags): array - { - if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { - return []; + 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 \PHPStan\Analyser\Scope $scope - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @param Expr|null $defaultExpr - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return list */ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): array { @@ -281,17 +350,62 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): continue; } - if ($scope->getFunction() === null && !$scope->isInAnonymousFunction()) { + $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.') + ->identifier('varTag.noVariable') + ->build(); + } + } + + return $errors; + } + + /** + * @param VarTag[] $varTags + * @return list + */ + private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $varTags): array + { + $variableNames = []; + foreach ($node->vars as $var) { + if (!$var instanceof Expr\Variable) { + continue; + } + if (!is_string($var->name)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name))->build(); + $variableNames[$var->name] = true; } - if (count($variableLessVarTags) !== 1 || $defaultExpr === null) { - if (count($variableLessVarTags) > 0) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.')->build(); + $errors = []; + foreach (array_keys($varTags) as $name) { + if (is_int($name)) { + if (count($variableNames) === 1) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + 'PHPDoc tag @var above multiple global variables does not specify variable name.', + )->identifier('varTag.noVariable')->build(); + continue; } + + if (isset($variableNames[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '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))), + ))->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 new file mode 100644 index 0000000000..548e176c1d --- /dev/null +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -0,0 +1,64 @@ + + */ +final class AccessPrivatePropertyThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\StaticPropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\VarLikeIdentifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $propertyName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasProperty($propertyName)->yes()) { + return []; + } + + $property = $classType->getProperty($propertyName, $scope); + if (!$property->isPrivate()) { + return []; + } + if (!$property->isStatic()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe access to private property %s::$%s through static::.', + $property->getDeclaringClass()->getDisplayName(), + $propertyName, + ))->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 2ed10a19fe..6577820611 100644 --- a/src/Rules/Properties/AccessPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessPropertiesInAssignRule.php @@ -4,33 +4,35 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @implements Rule */ -class AccessPropertiesInAssignRule implements Rule +final class AccessPropertiesInAssignRule implements Rule { - private \PHPStan\Rules\Properties\AccessPropertiesRule $accessPropertiesRule; - - public function __construct(AccessPropertiesRule $accessPropertiesRule) + public function __construct(private AccessPropertiesCheck $check) { - $this->accessPropertiesRule = $accessPropertiesRule; } public function getNodeType(): string { - return Node\Expr\Assign::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\PropertyFetch) { + if (!$node->getPropertyFetch() instanceof Node\Expr\PropertyFetch) { + return []; + } + + if ($node->isAssignOp()) { return []; } - return $this->accessPropertiesRule->processNode($node->var, $scope); + return $this->check->check($node->getPropertyFetch(), $scope, true); } } diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index ae6ed7321f..e9b382c7f2 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -2,36 +2,19 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; use PhpParser\Node\Expr\PropertyFetch; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\PropertyFetch> + * @implements Rule */ -class AccessPropertiesRule implements \PHPStan\Rules\Rule +final class AccessPropertiesRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $reportMagicProperties; - - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - bool $reportMagicProperties - ) + public function __construct(private AccessPropertiesCheck $check) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMagicProperties = $reportMagicProperties; } public function getNodeType(): string @@ -39,103 +22,9 @@ public function getNodeType(): string return PropertyFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof \PhpParser\Node\Identifier) { - return []; - } - - $name = $node->name->name; - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->var, - sprintf('Access to property $%s on an unknown class %%s.', $name), - static function (Type $type) use ($name): bool { - return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); - } - ); - $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 !== false) { - if ($parentClassReflection->hasProperty($name)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Access to private property $%s of parent class %s.', - $name, - $parentClassReflection->getDisplayName() - ))->build(), - ]; - } - - $parentClassReflection = $parentClassReflection->getParentClass(); - } - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Access to an undefined property %s::$%s.', - $type->describe(VerbosityLevel::typeOnly()), - $name - ))->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 efd61a5c59..f9a6e61602 100644 --- a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php @@ -4,33 +4,35 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @implements Rule */ -class AccessStaticPropertiesInAssignRule implements Rule +final class AccessStaticPropertiesInAssignRule implements Rule { - private \PHPStan\Rules\Properties\AccessStaticPropertiesRule $accessStaticPropertiesRule; - - public function __construct(AccessStaticPropertiesRule $accessStaticPropertiesRule) + public function __construct(private AccessStaticPropertiesRule $accessStaticPropertiesRule) { - $this->accessStaticPropertiesRule = $accessStaticPropertiesRule; } public function getNodeType(): string { - return Node\Expr\Assign::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\StaticPropertyFetch) { + if (!$node->getPropertyFetch() instanceof Node\Expr\StaticPropertyFetch) { + return []; + } + + if ($node->isAssignOp()) { return []; } - return $this->accessStaticPropertiesRule->processNode($node->var, $scope); + return $this->accessStaticPropertiesRule->processNode($node->getPropertyFetch(), $scope); } } diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 6356d85966..b15808ad5a 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -5,41 +5,45 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\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; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; 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; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticPropertyFetch> + * @implements Rule */ -class AccessStaticPropertiesRule implements \PHPStan\Rules\Rule +final class AccessStaticPropertiesRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private ClassNameCheck $classCheck, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -49,11 +53,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\VarLikeIdentifier) { - return []; + if ($node->name instanceof Node\VarLikeIdentifier) { + $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)); } - $name = $node->name->name; + return $errors; + } + + /** + * @return list + */ + private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array + { $messages = []; if ($node->class instanceof Name) { $class = (string) $node->class; @@ -64,83 +82,76 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Accessing %s::$%s outside of class scope.', $class, - $name - ))->build(), + $name, + ))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(), ]; } - $className = $scope->getClassReflection()->getName(); + $classType = $scope->resolveTypeByName($node->class); } elseif ($lowercasedClass === 'parent') { if (!$scope->isInClass()) { return [ RuleErrorBuilder::message(sprintf( 'Accessing %s::$%s outside of class scope.', $class, - $name - ))->build(), + $name, + ))->identifier('outOfClass.parent')->build(), ]; } - if ($scope->getClassReflection()->getParentClass() === false) { + if ($scope->getClassReflection()->getParentClass() === null) { return [ RuleErrorBuilder::message(sprintf( '%s::%s() accesses parent::$%s but %s does not extend any class.', $scope->getClassReflection()->getDisplayName(), $scope->getFunctionName(), $name, - $scope->getClassReflection()->getDisplayName() - ))->build(), + $scope->getClassReflection()->getDisplayName(), + ))->identifier('class.noParent')->build(), ]; } - if ($scope->getFunctionName() === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $currentMethodReflection = $scope->getClassReflection()->getNativeMethod($scope->getFunctionName()); - if (!$currentMethodReflection->isStatic()) { - // calling parent::method() from instance method - return []; - } - - $className = $scope->getClassReflection()->getParentClass()->getName(); + $classType = $scope->resolveTypeByName($node->class); } else { if (!$this->reflectionProvider->hasClass($class)) { if ($scope->isInClassExists($class)) { 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 - ))->build(), + $errorBuilder->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); } - $classReflection = $this->reflectionProvider->getClass($class); - $className = $this->reflectionProvider->getClass($class)->getName(); - if ($classReflection->isTrait()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Access to static property $%s on trait %s.', - $name, - $className - ))->build(), - ]; + $locationData = []; + $locationClassReflection = $this->reflectionProvider->getClass($class); + if ($locationClassReflection->hasProperty($name)) { + $locationData['property'] = $locationClassReflection->getProperty($name, $scope); } - } - $classType = new ObjectType($className); + $messages = $this->classCheck->checkClassNames( + $scope, + [new ClassNameNodePair($class, $node->class)], + ClassNameUsageLocation::from(ClassNameUsageLocation::STATIC_PROPERTY_ACCESS, $locationData), + ); + + $classType = $scope->resolveTypeByName($node->class); + } } else { $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->class, - sprintf('Access to static property $%s on an unknown class %%s.', $name), - static function (Type $type) use ($name): bool { - return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); - } + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), + sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { @@ -148,38 +159,69 @@ static function (Type $type) use ($name): bool { } } - if ((new StringType())->isSuperTypeOf($classType)->yes()) { + if ($classType->isString()->yes()) { return []; } $typeForDescribe = $classType; + if ($classType instanceof ThisType) { + $typeForDescribe = $classType->getStaticObjectType(); + } $classType = TypeCombinator::remove($classType, new StringType()); if ($scope->isInExpressionAssign($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(), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->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(), + $name, + ))->identifier('staticProperty.notFound')->build(), ]); } @@ -196,19 +238,19 @@ static function (Type $type) use ($name): bool { RuleErrorBuilder::message(sprintf( 'Static access to instance property %s::$%s.', $property->getDeclaringClass()->getDisplayName(), - $name - ))->build(), + $name, + ))->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(), + $property->getDeclaringClass()->getDisplayName(), + ))->identifier(sprintf('staticProperty.%s', $property->isPrivate() ? 'private' : 'protected'))->build(), ]); } diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index e6e004f995..cb68412aa4 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -3,70 +3,66 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; -use PhpParser\Node\Stmt\Property; use PHPStan\Analyser\Scope; +use PHPStan\Node\ClassPropertyNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Property> + * @implements Rule */ -class DefaultValueTypesAssignedToPropertiesRule implements \PHPStan\Rules\Rule +final class DefaultValueTypesAssignedToPropertiesRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return Property::class; + return ClassPropertyNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + $default = $node->getDefault(); + if ($default === null) { + return []; } - $classReflection = $scope->getClassReflection(); - - $errors = []; - foreach ($node->props as $property) { - if ($property->default === null) { - continue; - } + $classReflection = $node->getClassReflection(); - $propertyReflection = $classReflection->getNativeProperty($property->name->name); - $propertyType = $propertyReflection->getWritableType(); - if ($propertyReflection->getNativeType() instanceof MixedType) { - if ($property->default instanceof Node\Expr\ConstFetch && (string) $property->default->name === 'null') { - continue; - } - } - $defaultValueType = $scope->getType($property->default); - if ($this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true)) { - continue; + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + $propertyType = $propertyReflection->getWritableType(); + if (!$propertyReflection->hasNativeType()) { + if ($default instanceof Node\Expr\ConstFetch && $default->name->toLowerString() === 'null') { + return []; } + } + $defaultValueType = $scope->getType($default); + $accepts = $this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true); + if ($accepts->result) { + return []; + } - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $defaultValueType); - $errors[] = RuleErrorBuilder::message(sprintf( + return [ + RuleErrorBuilder::message(sprintf( '%s %s::$%s (%s) does not accept default value of type %s.', $node->isStatic() ? 'Static property' : 'Property', $classReflection->getDisplayName(), - $property->name->name, + $node->getName(), $propertyType->describe($verbosityLevel), - $defaultValueType->describe($verbosityLevel) - ))->build(); - } - - return $errors; + $defaultValueType->describe($verbosityLevel), + )) + ->identifier('property.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(), + ]; } } diff --git a/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php new file mode 100644 index 0000000000..0130c2f633 --- /dev/null +++ b/src/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php @@ -0,0 +1,23 @@ +extensions; + } + +} diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index 6c190eef61..869074e358 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -3,58 +3,52 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; -use PhpParser\Node\Stmt\PropertyProperty; use PHPStan\Analyser\Scope; +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 function array_map; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\PropertyProperty> + * @implements Rule */ -class ExistingClassesInPropertiesRule implements \PHPStan\Rules\Rule +final class ExistingClassesInPropertiesRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - - private bool $checkThisOnly; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity, - bool $checkThisOnly + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PhpVersion $phpVersion, + private bool $checkClassCaseSensitivity, + private bool $checkThisOnly, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return PropertyProperty::class; + return ClassPropertyNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->name->name); + $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); if ($this->checkThisOnly) { $referencedClasses = $propertyReflection->getNativeType()->getReferencedClasses(); } else { $referencedClasses = array_merge( $propertyReflection->getNativeType()->getReferencedClasses(), - $propertyReflection->getPhpDocType()->getReferencedClasses() + $propertyReflection->getPhpDocType()->getReferencedClasses(), ); } @@ -65,28 +59,49 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s has invalid type %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), - $node->name->name, - $referencedClass - ))->build(); + $node->getName(), + $referencedClass, + ))->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->name->name, - $referencedClass - ))->build(); + $node->getName(), + $referencedClass, + )) + ->identifier('class.notFound'); + + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static function (string $class) use ($node): ClassNameNodePair { - return new ClassNameNodePair($class, $node); - }, $referencedClasses)) - ); + $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()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s has unresolvable native type.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->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 5f98640ac2..1b14b785aa 100644 --- a/src/Rules/Properties/FoundPropertyReflection.php +++ b/src/Rules/Properties/FoundPropertyReflection.php @@ -2,31 +2,31 @@ namespace PHPStan\Rules\Properties; +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\ResolvedPropertyReflection; +use PHPStan\Reflection\WrapperPropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class FoundPropertyReflection implements PropertyReflection +final class FoundPropertyReflection implements ExtendedPropertyReflection { - private PropertyReflection $originalPropertyReflection; - - private Type $readableType; - - private Type $writableType; - public function __construct( - PropertyReflection $originalPropertyReflection, - Type $readableType, - Type $writableType + private ExtendedPropertyReflection $originalPropertyReflection, + private Scope $scope, + private string $propertyName, + private Type $readableType, + private Type $writableType, ) { - $this->originalPropertyReflection = $originalPropertyReflection; - $this->readableType = $readableType; - $this->writableType = $writableType; + } + + public function getScope(): Scope + { + return $this->scope; } public function getDeclaringClass(): ClassReflection @@ -34,6 +34,11 @@ public function getDeclaringClass(): ClassReflection return $this->originalPropertyReflection->getDeclaringClass(); } + public function getName(): string + { + return $this->propertyName; + } + public function isStatic(): bool { return $this->originalPropertyReflection->isStatic(); @@ -54,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; @@ -96,18 +121,13 @@ public function isInternal(): TrinaryLogic public function isNative(): bool { - $reflection = $this->originalPropertyReflection; - if ($reflection instanceof ResolvedPropertyReflection) { - $reflection = $reflection->getOriginalReflection(); - } - - return $reflection instanceof PhpPropertyReflection; + return $this->getNativeReflection() !== null; } - public function getNativeType(): ?Type + public function getNativeReflection(): ?PhpPropertyReflection { $reflection = $this->originalPropertyReflection; - if ($reflection instanceof ResolvedPropertyReflection) { + while ($reflection instanceof WrapperPropertyReflection) { $reflection = $reflection->getOriginalReflection(); } @@ -115,7 +135,47 @@ public function getNativeType(): ?Type return null; } - return $reflection->getNativeType(); + 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 new file mode 100644 index 0000000000..f42cb7e7d5 --- /dev/null +++ b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php @@ -0,0 +1,26 @@ +extensions === null) { + $this->extensions = $this->container->getServicesByTag(ReadWritePropertiesExtensionProvider::EXTENSION_TAG); + } + + return $this->extensions; + } + +} diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index 33808e7b2b..84c8a20325 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -4,44 +4,46 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\PropertyProperty> + * @implements Rule */ -final class MissingPropertyTypehintRule implements \PHPStan\Rules\Rule +final class MissingPropertyTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct(private MissingTypehintCheck $missingTypehintCheck) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\PropertyProperty::class; + return ClassPropertyNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); + + if ($propertyReflection->isPromoted()) { + return []; } - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->name->name); $propertyType = $propertyReflection->getReadableType(); + if ($propertyType instanceof MixedType && !$propertyType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Property %s::$%s has no typehint specified.', + 'Property %s::$%s has no type specified.', $propertyReflection->getDeclaringClass()->getDisplayName(), - $node->name->name - ))->build(), + $node->getName(), + ))->identifier('missingType.property')->build(), ]; } @@ -51,19 +53,33 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s type has no value type specified in iterable type %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), - $node->name->name, - $iterableTypeDescription - ))->tip(sprintf(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, $iterableTypeDescription))->build(); + $node->getName(), + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($propertyType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s with generic %s does not specify its types: %s', $propertyReflection->getDeclaringClass()->getDisplayName(), - $node->name->name, + $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) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s type has no signature specified for %s.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $node->getName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->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 new file mode 100644 index 0000000000..de05aaaab9 --- /dev/null +++ b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php @@ -0,0 +1,82 @@ + + */ +final class MissingReadOnlyPropertyAssignRule 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->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.uninitializedReadonly') + ->build(); + } + + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { + if (!$propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + 'Access to an uninitialized readonly property %s::$%s.', + $classReflection->getDisplayName(), + $propertyName, + )) + ->line($line) + ->file($file, $fileDescription) + ->identifier('property.uninitializedReadonly') + ->build(); + } + + foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { + if (!$propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readonly property %s::$%s is already assigned.', + $classReflection->getDisplayName(), + $propertyName, + )) + ->line($line) + ->identifier('assign.readOnlyProperty') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php new file mode 100644 index 0000000000..6b72cf6df7 --- /dev/null +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -0,0 +1,45 @@ + + */ +final class NullsafePropertyFetchRule implements Rule +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Node\Expr\NullsafePropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $calledOnType = $scope->getType($node->var); + if (!$calledOnType->isNull()->no()) { + return []; + } + + if ($scope->isUndefinedExpressionAllowed($node)) { + return []; + } + + return [ + 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 new file mode 100644 index 0000000000..c1806565e2 --- /dev/null +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -0,0 +1,345 @@ + + */ +final class OverridingPropertyRule implements Rule +{ + + public function __construct( + private PhpVersion $phpVersion, + private bool $checkPhpDocMethodSignatures, + private bool $reportMaybes, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $prototype = $this->findPrototype($classReflection, $node->getName()); + if ($prototype === null) { + return []; + } + + $errors = []; + if ($prototype->isStatic()) { + if (!$node->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Non-static property %s::$%s overrides static property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.nonStatic')->nonIgnorable()->build(); + } + } elseif ($node->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Static property %s::$%s overrides non-static property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.static')->nonIgnorable()->build(); + } + + if ($prototype->isReadOnly()) { + if (!$node->isReadOnly()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readwrite property %s::$%s overrides readonly property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.readWrite')->nonIgnorable()->build(); + } + } elseif ($node->isReadOnly()) { + 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()) { + if (!$node->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s property %s::$%s overriding public property %s::$%s should also be public.', + $node->isPrivate() ? 'Private' : 'Protected', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.visibility')->nonIgnorable()->build(); + } + } elseif ($node->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private property %s::$%s overriding protected property %s::$%s should be protected or public.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->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 ($nativeType === null) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overriding property %s::$%s (%s) should also have native type %s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.missingNativeType')->nonIgnorable()->build(); + } else { + if (!$prototype->getNativeType()->equals($nativeType)) { + 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 ($nativeType !== null) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s (%s) overriding property %s::$%s should not have a native type.', + $classReflection->getDisplayName(), + $node->getName(), + $nativeType->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.extraNativeType')->nonIgnorable()->build(); + } + + $errors = array_merge($errors, $typeErrors); + + if (!$this->checkPhpDocMethodSignatures) { + return $errors; + } + + if (count($typeErrors) > 0) { + return $errors; + } + + 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.', + $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\n This error can be turned off by setting\n %s", + 'https://phpstan.org/user-guide/stub-files', + 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', + ))->build(); + $cannotBeTurnedOffError = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is %s PHPDoc type %s of overridden property %s::$%s.', + $propertyReflection->getReadableType()->describe($verbosity), + $classReflection->getDisplayName(), + $node->getName(), + $this->reportMaybes ? 'not the same as' : 'not covariant with', + $prototype->getReadableType()->describe($verbosity), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->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(); + if ($this->reportMaybes) { + if (!$isSuperType->yes()) { + $errors[] = $cannotBeTurnedOffError; + } else { + $errors[] = $canBeTurnedOffError; + } + } else { + if (!$isSuperType->yes()) { + $errors[] = $cannotBeTurnedOffError; + } + } + + return $errors; + } + + private function findPrototype(ClassReflection $classReflection, string $propertyName): ?PhpPropertyReflection + { + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return $this->findPrototypeInInterfaces($classReflection, $propertyName); + } + + if (!$parentClass->hasNativeProperty($propertyName)) { + return $this->findPrototypeInInterfaces($classReflection, $propertyName); + } + + $property = $parentClass->getNativeProperty($propertyName); + if ($property->isPrivate()) { + 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 new file mode 100644 index 0000000000..375eb439cf --- /dev/null +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -0,0 +1,36 @@ + + */ +final class PropertyAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Property::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->attrGroups, + Attribute::TARGET_PROPERTY, + 'property', + ); + } + +} diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index 7f526bb6af..8588a1a57a 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -2,25 +2,40 @@ 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 { /** - * @param \PHPStan\Reflection\PropertyReflection $property - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return string + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - public function describeProperty(PropertyReflection $property, $propertyFetch): string + public function describeProperty(PropertyReflection $property, Scope $scope, $propertyFetch): string { - /** @var \PhpParser\Node\Identifier $name */ + 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 ($propertyFetch instanceof \PhpParser\Node\Expr\PropertyFetch) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + if (!$property->isStatic()) { + 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 0a846766f1..67b2785fa9 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -2,90 +2,133 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\VarLikeIdentifier; use PHPStan\Analyser\Scope; -use PHPStan\Type\ObjectType; -use PHPStan\Type\StaticType; -use PHPStan\Type\ThisType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; +use function array_map; +use function count; -class PropertyReflectionFinder +final class PropertyReflectionFinder { /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @param \PHPStan\Analyser\Scope $scope - * @return FoundPropertyReflection|null + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch + * @return FoundPropertyReflection[] */ - public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?FoundPropertyReflection + public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): array { - if ($propertyFetch instanceof \PhpParser\Node\Expr\PropertyFetch) { - if (!$propertyFetch->name instanceof \PhpParser\Node\Identifier) { - return null; + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + if ($propertyFetch->name instanceof Node\Identifier) { + $names = [$propertyFetch->name->name]; + } else { + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } + + $reflections = []; $propertyHolderType = $scope->getType($propertyFetch->var); - $fetchedOnThis = $propertyHolderType instanceof ThisType && $scope->isInClass(); - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope, $fetchedOnThis); - } + foreach ($names as $name) { + $reflection = $this->findPropertyReflection( + $propertyHolderType, + $name, + $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( + $propertyFetch->name, + new String_($name), + )) : $scope, + ); + if ($reflection === null) { + continue; + } - if (!$propertyFetch->name instanceof \PhpParser\Node\Identifier) { - return null; + $reflections[] = $reflection; + } + + return $reflections; } - if ($propertyFetch->class instanceof \PhpParser\Node\Name) { - $propertyHolderType = new ObjectType($scope->resolveName($propertyFetch->class)); + if ($propertyFetch->class instanceof Node\Name) { + $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); } - $fetchedOnThis = $propertyHolderType instanceof ThisType && $scope->isInClass(); + if ($propertyFetch->name instanceof VarLikeIdentifier) { + $names = [$propertyFetch->name->name]; + } else { + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); + } + + $reflections = []; + foreach ($names as $name) { + $reflection = $this->findPropertyReflection( + $propertyHolderType, + $name, + $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( + $propertyFetch->name, + new String_($name), + )) : $scope, + ); + if ($reflection === null) { + continue; + } - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope, $fetchedOnThis); + $reflections[] = $reflection; + } + + return $reflections; } - private function findPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope, bool $fetchedOnThis): ?FoundPropertyReflection + /** + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch + */ + public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?FoundPropertyReflection { - $transformedPropertyHolderType = TypeTraverser::map($propertyHolderType, static function (Type $type, callable $traverse) use ($scope, $fetchedOnThis): Type { - if ($type instanceof StaticType) { - if ($fetchedOnThis && $scope->isInClass()) { - return $traverse($type->changeBaseClass($scope->getClassReflection())); - } - if ($scope->isInClass()) { - return $traverse($type->changeBaseClass($scope->getClassReflection())->getStaticObjectType()); - } + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + $propertyHolderType = $scope->getType($propertyFetch->var); + if ($propertyFetch->name instanceof Node\Identifier) { + return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); } - return $traverse($type); - }); + $nameType = $scope->getType($propertyFetch->name); + $nameTypeConstantStrings = $nameType->getConstantStrings(); + if (count($nameTypeConstantStrings) === 1) { + return $this->findPropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + } - if (!$transformedPropertyHolderType->hasProperty($propertyName)->yes()) { return null; } - $originalProperty = $transformedPropertyHolderType->getProperty($propertyName, $scope); - $readableType = $this->transformPropertyType($originalProperty->getReadableType(), $transformedPropertyHolderType, $scope, $fetchedOnThis); - $writableType = $this->transformPropertyType($originalProperty->getWritableType(), $transformedPropertyHolderType, $scope, $fetchedOnThis); + if (!$propertyFetch->name instanceof Node\Identifier) { + return null; + } - return new FoundPropertyReflection( - $originalProperty, - $readableType, - $writableType - ); + if ($propertyFetch->class instanceof Node\Name) { + $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); + } else { + $propertyHolderType = $scope->getType($propertyFetch->class); + } + + return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); } - private function transformPropertyType(Type $propertyType, Type $transformedPropertyHolderType, Scope $scope, bool $fetchedOnThis): Type + private function findPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection { - return TypeTraverser::map($propertyType, static function (Type $propertyType, callable $traverse) use ($transformedPropertyHolderType, $scope, $fetchedOnThis): Type { - if ($propertyType instanceof StaticType) { - if ($fetchedOnThis && $scope->isInClass()) { - return $traverse($propertyType->changeBaseClass($scope->getClassReflection())); - } + if (!$propertyHolderType->hasProperty($propertyName)->yes()) { + return null; + } - return $traverse($transformedPropertyHolderType); - } + $originalProperty = $propertyHolderType->getProperty($propertyName, $scope); - return $traverse($propertyType); - }); + return new FoundPropertyReflection( + $originalProperty, + $scope, + $propertyName, + $originalProperty->getReadableType(), + $originalProperty->getWritableType(), + ); } } 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 new file mode 100644 index 0000000000..d5079dc353 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php @@ -0,0 +1,57 @@ + + */ +final class ReadOnlyPropertyAssignRefRule implements Rule +{ + + public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) + { + } + + public function getNodeType(): string + { + return Node\Expr\AssignRef::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->expr instanceof Node\Expr\PropertyFetch) { + 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->isReadOnly()) { + continue; + } + + $declaringClass = $nativeReflection->getDeclaringClass(); + $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 new file mode 100644 index 0000000000..eac07303a2 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -0,0 +1,112 @@ + + */ +final class ReadOnlyPropertyAssignRule 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 []; + } + + $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->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.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())) + ->identifier('property.readOnlyAssignOutOfClass') + ->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.readOnlyAssignNotOnThis') + ->build(); + } + + 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.readOnlyAssignNotInConstructor') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadOnlyPropertyRule.php b/src/Rules/Properties/ReadOnlyPropertyRule.php new file mode 100644 index 0000000000..5e777ae164 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyRule.php @@ -0,0 +1,62 @@ + + */ +final class ReadOnlyPropertyRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isReadOnly()) { + return []; + } + + $errors = []; + if (!$this->phpVersion->supportsReadOnlyProperties()) { + $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable() + ->identifier('property.readOnlyNotSupported') + ->build(); + } + + if ($node->getNativeType() === null) { + $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() + ->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 new file mode 100644 index 0000000000..804619781c --- /dev/null +++ b/src/Rules/Properties/ReadWritePropertiesExtension.php @@ -0,0 +1,34 @@ + + * @implements Rule */ -class ReadingWriteOnlyPropertiesRule implements \PHPStan\Rules\Rule +final class ReadingWriteOnlyPropertiesRule implements Rule { - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private RuleLevelHelper $ruleLevelHelper; - - private bool $checkThisOnly; - public function __construct( - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder, - RuleLevelHelper $ruleLevelHelper, - bool $checkThisOnly + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + private RuleLevelHelper $ruleLevelHelper, + private bool $checkThisOnly, ) { - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } public function processNode(Node $node, Scope $scope): array @@ -64,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(), + $propertyDescription, + )) + ->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 5c3b5cab06..50d1502401 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -3,84 +3,116 @@ 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\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class TypesAssignedToPropertiesRule implements \PHPStan\Rules\Rule +final class TypesAssignedToPropertiesRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder + private RuleLevelHelper $ruleLevelHelper, + private PropertyReflectionFinder $propertyReflectionFinder, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignOp - ) { - return []; + $propertyFetch = $node->getPropertyFetch(); + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + + $errors = []; + foreach ($propertyReflections as $propertyReflection) { + $errors = array_merge($errors, $this->processSingleProperty( + $propertyReflection, + $propertyFetch, + $node->getAssignedExpr(), + )); } - if ( - !($node->var instanceof Node\Expr\PropertyFetch) - && !($node->var instanceof Node\Expr\StaticPropertyFetch) - ) { - return []; - } + return $errors; + } - /** @var \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ - $propertyFetch = $node->var; - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); - if ($propertyReflection === null) { + /** + * @return list + */ + private function processSingleProperty( + FoundPropertyReflection $propertyReflection, + PropertyFetch|StaticPropertyFetch $fetch, + Node\Expr $assignedExpr, + ): array + { + if (!$propertyReflection->isWritable()) { return []; } - $propertyType = $propertyReflection->getWritableType(); - - if ($node instanceof Node\Expr\Assign) { - $assignedValueType = $scope->getType($node->expr); + $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 { - $assignedValueType = $scope->getType($node); + $propertyType = $propertyReflection->getWritableType(); } - if (!$this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes())) { - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $propertyFetch); - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType); + + $assignedValueType = $scope->getType($assignedExpr); + + $accepts = $this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes()); + if (!$accepts->result) { + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyReflection->getName()); + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType); return [ RuleErrorBuilder::message(sprintf( '%s (%s) does not accept %s.', $propertyDescription, $propertyType->describe($verbosityLevel), - $assignedValueType->describe($verbosityLevel) - ))->build(), + $assignedValueType->describe($verbosityLevel), + )) + ->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 new file mode 100644 index 0000000000..525a9ecbb6 --- /dev/null +++ b/src/Rules/Properties/UninitializedPropertyRule.php @@ -0,0 +1,68 @@ + + */ +final class UninitializedPropertyRule 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] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); + + $errors = []; + foreach ($properties as $propertyName => $propertyNode) { + 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->getStartLine()) + ->identifier('property.uninitialized') + ->build(); + } + + 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) + ->file($file, $fileDescription) + ->identifier('property.uninitialized') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index 7d182231da..137caf24ce 100644 --- a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php +++ b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php @@ -4,84 +4,60 @@ 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 \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class WritingToReadOnlyPropertiesRule implements \PHPStan\Rules\Rule +final class WritingToReadOnlyPropertiesRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private bool $checkThisOnly; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder, - bool $checkThisOnly + private RuleLevelHelper $ruleLevelHelper, + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkThisOnly, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { + $propertyFetch = $node->getPropertyFetch(); if ( - !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignOp - ) { - 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 \PhpParser\Node\Expr\PropertyFetch|\PhpParser\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(), + $propertyDescription, + ))->identifier('assign.propertyReadOnly')->build(), ]; } diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php new file mode 100644 index 0000000000..ccc85a1c24 --- /dev/null +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -0,0 +1,149 @@ + + */ + 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(); + } + + $throwType = $functionReflection->getThrowType(); + if ( + $returnType->isVoid()->yes() + && !$isConstructor + && ($throwType === null || $throwType->isVoid()->yes()) + && $functionReflection->getAsserts()->getAll() === [] + ) { + $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 b1548f40b7..e15bdeb13b 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -2,19 +2,31 @@ namespace PHPStan\Rules\Regexp; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\Regex\RegexExpressionHelper; +use function in_array; +use function sprintf; +use function str_starts_with; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class RegularExpressionPatternRule implements \PHPStan\Rules\Rule +final class RegularExpressionPatternRule implements Rule { + public function __construct( + private RegexExpressionHelper $regexExpressionHelper, + ) + { + } + public function getNodeType(): string { return FuncCall::class; @@ -31,15 +43,13 @@ 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; } /** - * @param FuncCall $functionCall - * @param Scope $scope * @return string[] */ private function extractPatterns(FuncCall $functionCall, Scope $scope): array @@ -48,63 +58,60 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array return []; } $functionName = strtolower((string) $functionCall->name); - if (!\Nette\Utils\Strings::startsWith($functionName, 'preg_')) { + if (!str_starts_with($functionName, 'preg_')) { return []; } - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return []; } - $patternNode = $functionCall->args[0]->value; + $patternNode = $functionCall->getArgs()[0]->value; $patternType = $scope->getType($patternNode); $patternStrings = []; - 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(); } } @@ -114,8 +121,8 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array private function validatePattern(string $pattern): ?string { try { - \Nette\Utils\Strings::match('', $pattern); - } catch (\Nette\Utils\RegexpException $e) { + Strings::match('', $pattern); + } catch (RegexpException $e) { return $e->getMessage(); } diff --git a/src/Rules/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 fe75ac79f4..792f4428f0 100644 --- a/src/Rules/Registry.php +++ b/src/Rules/Registry.php @@ -2,54 +2,16 @@ namespace PHPStan\Rules; -class Registry -{ - - /** @var \PHPStan\Rules\Rule[][] */ - private array $rules = []; - - /** @var \PHPStan\Rules\Rule[][] */ - private array $cache = []; +use PhpParser\Node; - /** - * @param \PHPStan\Rules\Rule[] $rules - */ - public function __construct(array $rules) - { - foreach ($rules as $rule) { - $this->rules[$rule->getNodeType()][] = $rule; - } - } +interface Registry +{ /** - * @template TNodeType of \PhpParser\Node - * @phpstan-param class-string $nodeType - * @param \PhpParser\Node $nodeType - * @phpstan-return array<\PHPStan\Rules\Rule> - * @return \PHPStan\Rules\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; - } - - /** - * @phpstan-var array<\PHPStan\Rules\Rule> $selectedRules - * @var \PHPStan\Rules\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 a632f11c6b..0000000000 --- a/src/Rules/RegistryFactory.php +++ /dev/null @@ -1,26 +0,0 @@ -container = $container; - } - - public function create(): Registry - { - return new Registry( - $this->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 string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @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 fac7da86ed..cb714dfd2c 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -2,7 +2,20 @@ namespace PHPStan\Rules; -class RuleErrorBuilder +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 + * @template-covariant T of RuleError + */ +final class RuleErrorBuilder { private const TYPE_MESSAGE = 1; @@ -18,6 +31,9 @@ class RuleErrorBuilder /** @var mixed[] */ private array $properties; + /** @var list */ + private array $tips = []; + private function __construct(string $message) { $this->properties['message'] = $message; @@ -25,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; @@ -88,24 +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; @@ -114,6 +232,8 @@ public function identifier(string $identifier): self /** * @param mixed[] $metadata + * @phpstan-this-out self + * @return self */ public function metadata(array $metadata): self { @@ -123,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; @@ -130,12 +254,15 @@ 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 \PHPStan\ShouldNotHappenException(sprintf('Class %s does not exist.', $className)); + throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className)); } $ruleError = new $className(); @@ -143,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 8b24e289f8..c7a2338b30 100644 --- a/src/Rules/RuleErrors/RuleError1.php +++ b/src/Rules/RuleErrors/RuleError1.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError1 implements \PHPStan\Rules\RuleError +final class RuleError1 implements RuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError101.php b/src/Rules/RuleErrors/RuleError101.php index fe947a8d21..ff16ad5339 100644 --- a/src/Rules/RuleErrors/RuleError101.php +++ b/src/Rules/RuleErrors/RuleError101.php @@ -2,16 +2,23 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError101 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -25,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 6303a368cc..6c125de9b7 100644 --- a/src/Rules/RuleErrors/RuleError103.php +++ b/src/Rules/RuleErrors/RuleError103.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError103 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +20,8 @@ class RuleError103 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleE public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -32,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 2f2f3077ea..a0b8945f52 100644 --- a/src/Rules/RuleErrors/RuleError105.php +++ b/src/Rules/RuleErrors/RuleError105.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError105 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 cfe2a83969..a0b9b85c84 100644 --- a/src/Rules/RuleErrors/RuleError107.php +++ b/src/Rules/RuleErrors/RuleError107.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError107 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 5f4765bf2b..a4f81cce53 100644 --- a/src/Rules/RuleErrors/RuleError109.php +++ b/src/Rules/RuleErrors/RuleError109.php @@ -2,16 +2,24 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError109 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -27,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 6603af96dd..be6bc0923a 100644 --- a/src/Rules/RuleErrors/RuleError11.php +++ b/src/Rules/RuleErrors/RuleError11.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError11 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError +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 a3b958496f..ac0b980e01 100644 --- a/src/Rules/RuleErrors/RuleError111.php +++ b/src/Rules/RuleErrors/RuleError111.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError111 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +21,8 @@ class RuleError111 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleE public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -34,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 1e770a3bbc..5d602a2fe5 100644 --- a/src/Rules/RuleErrors/RuleError113.php +++ b/src/Rules/RuleErrors/RuleError113.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError113 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 7f4a7c4853..f6d020a9e2 100644 --- a/src/Rules/RuleErrors/RuleError115.php +++ b/src/Rules/RuleErrors/RuleError115.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError115 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 2cffaad58b..80b8bd5fb2 100644 --- a/src/Rules/RuleErrors/RuleError117.php +++ b/src/Rules/RuleErrors/RuleError117.php @@ -2,16 +2,24 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError117 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -27,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 e9bddceb6e..ddee015752 100644 --- a/src/Rules/RuleErrors/RuleError119.php +++ b/src/Rules/RuleErrors/RuleError119.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError119 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError119 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +21,8 @@ class RuleError119 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleE public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -34,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 1e26fb8da8..3a05d8d3c3 100644 --- a/src/Rules/RuleErrors/RuleError121.php +++ b/src/Rules/RuleErrors/RuleError121.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError121 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 98d76c5ea4..4bae22b6a5 100644 --- a/src/Rules/RuleErrors/RuleError123.php +++ b/src/Rules/RuleErrors/RuleError123.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError123 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 1e1a3b3c6b..a24ea70b44 100644 --- a/src/Rules/RuleErrors/RuleError125.php +++ b/src/Rules/RuleErrors/RuleError125.php @@ -2,16 +2,25 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError125 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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; @@ -29,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 dd268dda70..0c2dea58a7 100644 --- a/src/Rules/RuleErrors/RuleError127.php +++ b/src/Rules/RuleErrors/RuleError127.php @@ -2,10 +2,18 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError127 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +22,8 @@ class RuleError127 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleE public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -36,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 91e53ebf45..a606a29b87 100644 --- a/src/Rules/RuleErrors/RuleError13.php +++ b/src/Rules/RuleErrors/RuleError13.php @@ -2,16 +2,22 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError13 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError +final class RuleError13 implements RuleError, FileRuleError, TipRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -24,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 ae65a55526..b952f9a3c5 100644 --- a/src/Rules/RuleErrors/RuleError15.php +++ b/src/Rules/RuleErrors/RuleError15.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError15 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError +final class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleError { public string $message; @@ -14,6 +19,8 @@ class RuleError15 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -31,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 a436eb2397..8cdf151a33 100644 --- a/src/Rules/RuleErrors/RuleError17.php +++ b/src/Rules/RuleErrors/RuleError17.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError17 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError +final class RuleError17 implements RuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError19.php b/src/Rules/RuleErrors/RuleError19.php index e5248ff1e9..d7a3a4388b 100644 --- a/src/Rules/RuleErrors/RuleError19.php +++ b/src/Rules/RuleErrors/RuleError19.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError19 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError +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 17d6cc978f..91516979e0 100644 --- a/src/Rules/RuleErrors/RuleError21.php +++ b/src/Rules/RuleErrors/RuleError21.php @@ -2,16 +2,22 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError21 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError +final class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -24,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 da249a89cf..4dcb3e0bae 100644 --- a/src/Rules/RuleErrors/RuleError23.php +++ b/src/Rules/RuleErrors/RuleError23.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError23 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError +final class RuleError23 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError { public string $message; @@ -14,6 +19,8 @@ class RuleError23 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -31,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 879216abd9..429a1ed0ef 100644 --- a/src/Rules/RuleErrors/RuleError25.php +++ b/src/Rules/RuleErrors/RuleError25.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError25 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +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 af88436146..6910c787fd 100644 --- a/src/Rules/RuleErrors/RuleError27.php +++ b/src/Rules/RuleErrors/RuleError27.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError27 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +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 40855a0417..85a6c85960 100644 --- a/src/Rules/RuleErrors/RuleError29.php +++ b/src/Rules/RuleErrors/RuleError29.php @@ -2,16 +2,23 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError29 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +final class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -26,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 26169a3398..17ab507d2a 100644 --- a/src/Rules/RuleErrors/RuleError3.php +++ b/src/Rules/RuleErrors/RuleError3.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError3 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError +final class RuleError3 implements RuleError, LineRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError31.php b/src/Rules/RuleErrors/RuleError31.php index 0495d84a4e..d9e7665e9b 100644 --- a/src/Rules/RuleErrors/RuleError31.php +++ b/src/Rules/RuleErrors/RuleError31.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError31 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +final class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; @@ -14,6 +20,8 @@ class RuleError31 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -33,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 56198b54d7..692da9a71a 100644 --- a/src/Rules/RuleErrors/RuleError33.php +++ b/src/Rules/RuleErrors/RuleError33.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError33 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError33 implements RuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError35.php b/src/Rules/RuleErrors/RuleError35.php index 9911746c12..91c52036bb 100644 --- a/src/Rules/RuleErrors/RuleError35.php +++ b/src/Rules/RuleErrors/RuleError35.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError35 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\MetadataRuleError +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 119a3d5e87..a92ded0e4f 100644 --- a/src/Rules/RuleErrors/RuleError37.php +++ b/src/Rules/RuleErrors/RuleError37.php @@ -2,16 +2,22 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError37 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError37 implements RuleError, FileRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -25,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 56f98f8a05..7b74800753 100644 --- a/src/Rules/RuleErrors/RuleError39.php +++ b/src/Rules/RuleErrors/RuleError39.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError39 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError { public string $message; @@ -14,6 +19,8 @@ class RuleError39 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -32,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 45e916e381..7cc55fdeb1 100644 --- a/src/Rules/RuleErrors/RuleError41.php +++ b/src/Rules/RuleErrors/RuleError41.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError41 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +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 62824431f8..a251840dd9 100644 --- a/src/Rules/RuleErrors/RuleError43.php +++ b/src/Rules/RuleErrors/RuleError43.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError43 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +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 254e17df77..aceabd9f78 100644 --- a/src/Rules/RuleErrors/RuleError45.php +++ b/src/Rules/RuleErrors/RuleError45.php @@ -2,16 +2,23 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError45 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -27,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 9a17976afd..866bbc3ddf 100644 --- a/src/Rules/RuleErrors/RuleError47.php +++ b/src/Rules/RuleErrors/RuleError47.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError47 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; @@ -14,6 +20,8 @@ class RuleError47 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -34,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 6780a90cde..81b0015029 100644 --- a/src/Rules/RuleErrors/RuleError49.php +++ b/src/Rules/RuleErrors/RuleError49.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError49 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +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 99f66f59f4..0dbad8299b 100644 --- a/src/Rules/RuleErrors/RuleError5.php +++ b/src/Rules/RuleErrors/RuleError5.php @@ -2,16 +2,21 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError5 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError +final class RuleError5 implements RuleError, FileRuleError { public string $message; public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -22,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 7c70ad7c0b..96d93510c9 100644 --- a/src/Rules/RuleErrors/RuleError51.php +++ b/src/Rules/RuleErrors/RuleError51.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError51 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +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 23a1bad346..1e11f5e641 100644 --- a/src/Rules/RuleErrors/RuleError53.php +++ b/src/Rules/RuleErrors/RuleError53.php @@ -2,16 +2,23 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError53 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -27,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 b0fe074d84..3bf4a22ccf 100644 --- a/src/Rules/RuleErrors/RuleError55.php +++ b/src/Rules/RuleErrors/RuleError55.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError55 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError55 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; @@ -14,6 +20,8 @@ class RuleError55 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -34,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 5395e55b8d..22c77fc545 100644 --- a/src/Rules/RuleErrors/RuleError57.php +++ b/src/Rules/RuleErrors/RuleError57.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError57 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +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 db750c988e..a7659febe1 100644 --- a/src/Rules/RuleErrors/RuleError59.php +++ b/src/Rules/RuleErrors/RuleError59.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError59 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +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 63286dd5b1..723a0aa79b 100644 --- a/src/Rules/RuleErrors/RuleError61.php +++ b/src/Rules/RuleErrors/RuleError61.php @@ -2,16 +2,24 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError61 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -29,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 dcfe4ac0ee..1c88f9fbc2 100644 --- a/src/Rules/RuleErrors/RuleError63.php +++ b/src/Rules/RuleErrors/RuleError63.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError63 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +final class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; @@ -14,6 +21,8 @@ class RuleError63 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -36,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 c37e5c66ca..fc2593bbfa 100644 --- a/src/Rules/RuleErrors/RuleError65.php +++ b/src/Rules/RuleErrors/RuleError65.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError65 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError65 implements RuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError67.php b/src/Rules/RuleErrors/RuleError67.php index 924b6b70f8..b2218268c3 100644 --- a/src/Rules/RuleErrors/RuleError67.php +++ b/src/Rules/RuleErrors/RuleError67.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError67 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 3cb5843e4e..7f5e130f09 100644 --- a/src/Rules/RuleErrors/RuleError69.php +++ b/src/Rules/RuleErrors/RuleError69.php @@ -2,16 +2,22 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError69 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -22,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 606e57ecb4..203696b2fd 100644 --- a/src/Rules/RuleErrors/RuleError7.php +++ b/src/Rules/RuleErrors/RuleError7.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError7 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError +final class RuleError7 implements RuleError, LineRuleError, FileRuleError { public string $message; @@ -14,6 +18,8 @@ class RuleError7 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleErr public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -29,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 27c04fc061..d78d2813e3 100644 --- a/src/Rules/RuleErrors/RuleError71.php +++ b/src/Rules/RuleErrors/RuleError71.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError71 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +19,8 @@ class RuleError71 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -29,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 55d9a91539..fd81121a2d 100644 --- a/src/Rules/RuleErrors/RuleError73.php +++ b/src/Rules/RuleErrors/RuleError73.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError73 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 600407fbf1..d249eef75e 100644 --- a/src/Rules/RuleErrors/RuleError75.php +++ b/src/Rules/RuleErrors/RuleError75.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError75 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 56512c8faf..e0e8547a32 100644 --- a/src/Rules/RuleErrors/RuleError77.php +++ b/src/Rules/RuleErrors/RuleError77.php @@ -2,16 +2,23 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError77 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -24,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 95a10f354f..3a07eea396 100644 --- a/src/Rules/RuleErrors/RuleError79.php +++ b/src/Rules/RuleErrors/RuleError79.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError79 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +20,8 @@ class RuleError79 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -31,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 122bd698e1..fe96c09839 100644 --- a/src/Rules/RuleErrors/RuleError81.php +++ b/src/Rules/RuleErrors/RuleError81.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError81 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 3e7248cad3..9570715ebe 100644 --- a/src/Rules/RuleErrors/RuleError83.php +++ b/src/Rules/RuleErrors/RuleError83.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError83 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 68866e7a18..5af535902a 100644 --- a/src/Rules/RuleErrors/RuleError85.php +++ b/src/Rules/RuleErrors/RuleError85.php @@ -2,16 +2,23 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError85 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -24,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 742bb0c7b2..44028fe427 100644 --- a/src/Rules/RuleErrors/RuleError87.php +++ b/src/Rules/RuleErrors/RuleError87.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError87 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError87 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +20,8 @@ class RuleError87 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -31,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 83f5a00039..e69b4058a6 100644 --- a/src/Rules/RuleErrors/RuleError89.php +++ b/src/Rules/RuleErrors/RuleError89.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError89 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 a4cd956791..c8454faf62 100644 --- a/src/Rules/RuleErrors/RuleError9.php +++ b/src/Rules/RuleErrors/RuleError9.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError9 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError +final class RuleError9 implements RuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError91.php b/src/Rules/RuleErrors/RuleError91.php index dfd25a1e71..8c11c1816f 100644 --- a/src/Rules/RuleErrors/RuleError91.php +++ b/src/Rules/RuleErrors/RuleError91.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError91 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 691a43d24a..8c5b9c5a64 100644 --- a/src/Rules/RuleErrors/RuleError93.php +++ b/src/Rules/RuleErrors/RuleError93.php @@ -2,16 +2,24 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError93 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -26,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 b3170a3515..68b993db00 100644 --- a/src/Rules/RuleErrors/RuleError95.php +++ b/src/Rules/RuleErrors/RuleError95.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError95 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; @@ -14,6 +21,8 @@ class RuleError95 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleEr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -33,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 1a7c512453..da13e04d88 100644 --- a/src/Rules/RuleErrors/RuleError97.php +++ b/src/Rules/RuleErrors/RuleError97.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError97 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +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 76064d3486..60c3af565f 100644 --- a/src/Rules/RuleErrors/RuleError99.php +++ b/src/Rules/RuleErrors/RuleError99.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError99 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +final class RuleError99 implements RuleError, LineRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index de359f0b2b..20b9597722 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -6,185 +6,317 @@ 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\StrictMixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function count; +use function sprintf; -class RuleLevelHelper +final class RuleLevelHelper { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkNullables; - - private bool $checkThisOnly; - - private bool $checkUnionTypes; - - private bool $checkExplicitMixed; - public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkNullables, - bool $checkThisOnly, - bool $checkUnionTypes, - bool $checkExplicitMixed = false + private ReflectionProvider $reflectionProvider, + private bool $checkNullables, + private bool $checkThisOnly, + private bool $checkUnionTypes, + private bool $checkExplicitMixed, + private bool $checkImplicitMixed, + private bool $checkBenevolentUnionTypes, + private bool $discoveringSymbolsTip, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkNullables = $checkNullables; - $this->checkThisOnly = $checkThisOnly; - $this->checkUnionTypes = $checkUnionTypes; - $this->checkExplicitMixed = $checkExplicitMixed; } + /** @api */ public function isThis(Expr $expression): bool { return $expression instanceof Expr\Variable && $expression->name === 'this'; } - public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool + private function transformCommonType(Type $type): Type { - if ( - $this->checkExplicitMixed - && $acceptedType instanceof MixedType - && $acceptedType->isExplicitMixed() - ) { - $acceptedType = new StrictMixedType(); + if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { + return $type; } - if ( - !$this->checkNullables - && !$acceptingType instanceof NullType - && !$acceptedType instanceof NullType - && !$acceptedType instanceof BenevolentUnionType - ) { - $acceptedType = TypeCombinator::removeNull($acceptedType); - } + 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() && $this->checkExplicitMixed) + || (!$type->isExplicitMixed() && $this->checkImplicitMixed) + ) + ) { + return new StrictMixedType(); + } + + return $traverse($type); + }); + } - if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) { - foreach ($acceptingType->getTypes() as $innerType) { - if (self::accepts($innerType, $acceptedType, $strictTypes)) { - return true; + /** + * @return array{Type, bool} + */ + private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array + { + $checkForUnion = $this->checkUnionTypes; + $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { + if ($acceptedType instanceof CallableType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; } + + return new CallableType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getTemplateTags(), + $acceptedType->isPure(), + ); } - return false; - } + if ($acceptedType instanceof ClosureType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } - if ( - $acceptedType->isArray()->yes() - && $acceptingType->isArray()->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 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 $traverse($this->transformCommonType($acceptedType)); + }); + + return [$acceptedType, $checkForUnion]; + } + + /** @api */ + public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult + { + [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); + $acceptingType = $this->transformCommonType($acceptingType); $accepts = $acceptingType->accepts($acceptedType, $strictTypes); - return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no(); + return new RuleLevelHelperAcceptsResult( + $checkForUnion ? $accepts->yes() : !$accepts->no(), + $accepts->reasons, + ); } /** - * @param Scope $scope - * @param Expr $var - * @param string $unknownClassErrorPattern + * @api * @param callable(Type $type): bool $unionTypeCriteriaCallback - * @return FoundTypeResult */ public function findTypeToCheck( Scope $scope, Expr $var, string $unknownClassErrorPattern, - callable $unionTypeCriteriaCallback + callable $unionTypeCriteriaCallback, ): FoundTypeResult { if ($this->checkThisOnly && !$this->isThis($var)) { - return new FoundTypeResult(new ErrorType(), [], []); + return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); - if (!$this->checkNullables && !$type instanceof NullType) { - $type = \PHPStan\Type\TypeCombinator::removeNull($type); + + 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(), [], []); + return new FoundTypeResult( + $type instanceof TemplateMixedType + ? $type->toStrictMixedType() + : new StrictMixedType(), + [], + [], + null, + ); } if ($type instanceof MixedType || $type instanceof NeverType) { - return new FoundTypeResult(new ErrorType(), [], []); - } - if ($type instanceof StaticType) { - $type = $type->getStaticObjectType(); + return new FoundTypeResult(new ErrorType(), [], [], null); } $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())->build(); + if ($this->discoveringSymbolsTip) { + $errorBuilder->discoveringSymbolsTip(); + } + + $errors[] = $errorBuilder->build(); + } } if (count($errors) > 0 || $hasClassExistsClass) { - return new FoundTypeResult(new ErrorType(), [], $errors); + return new FoundTypeResult(new ErrorType(), [], $errors, null); + } + + if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { + return new FoundTypeResult(new ErrorType(), [], [], null); } - if (!$this->checkUnionTypes) { - if ($type instanceof ObjectWithoutClassType) { - return new FoundTypeResult(new ErrorType(), [], []); + 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, []); + 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); } } - return new FoundTypeResult($type, $directClassNames, []); + $tip = null; + if ( + $type instanceof UnionType + && count($type->getTypes()) === 2 + && $type->isObject()->yes() + && $type->getTypes()[0]->getObjectClassNames() === ['PhpParser\\Node\\Arg'] + && $type->getTypes()[1]->getObjectClassNames() === ['PhpParser\\Node\\VariadicPlaceholder'] + && !$unionTypeCriteriaCallback($type) + ) { + $tip = 'Use ->getArgs() instead of ->args.'; + } + + return new FoundTypeResult($type, $directClassNames, [], $tip); } } 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 e9883482d8..74c5328ba4 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -7,14 +7,14 @@ 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; /** * @implements Rule */ -class TooWideArrowFunctionReturnTypehintRule implements Rule +final class TooWideArrowFunctionReturnTypehintRule implements Rule { public function getNodeType(): string @@ -24,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 = []; @@ -49,9 +50,9 @@ public function processNode(Node $node, Scope $scope): array } $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return typehint.', - $type->describe(VerbosityLevel::typeOnly()) - ))->build(); + 'Anonymous function never returns %s so it can be removed from the return type.', + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 9277287e8f..bf49b02765 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -7,15 +7,16 @@ 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; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClosureReturnStatementsNode> + * @implements Rule */ -class TooWideClosureReturnTypehintRule implements Rule +final class TooWideClosureReturnTypehintRule implements Rule { public function getNodeType(): string @@ -25,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 []; @@ -45,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(); @@ -60,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array } $returnType = TypeCombinator::union(...$returnTypes); - if ($returnType instanceof NullType) { + if ($returnType->isNull()->yes()) { return []; } @@ -71,9 +72,9 @@ public function processNode(Node $node, Scope $scope): array } $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return typehint.', - $type->describe(VerbosityLevel::typeOnly()) - ))->build(); + 'Anonymous function never returns %s so it can be removed from the return type.', + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + ))->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 747685bc9e..3b52a47aad 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -5,18 +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\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 \PHPStan\Rules\Rule<\PHPStan\Node\FunctionReturnStatementsNode> + * @implements Rule */ -class TooWideFunctionReturnTypehintRule implements Rule +final class TooWideFunctionReturnTypehintRule implements Rule { public function getNodeType(): string @@ -26,12 +28,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $function = $scope->getFunction(); - if (!$function instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } + $function = $node->getFunctionReflection(); - $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $functionReturnType = $function->getReturnType(); + $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); if (!$functionReturnType instanceof UnionType) { return []; } @@ -49,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); @@ -67,11 +68,21 @@ public function processNode(Node $node, Scope $scope): array continue; } + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + continue 2; + } + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() never returns %s so it can be removed from the return typehint.', + 'Function %s() never returns %s so it can be removed from the return type.', $function->getName(), - $type->describe(VerbosityLevel::typeOnly()) - ))->build(); + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + ))->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 107b06af13..8737855175 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -5,27 +5,24 @@ 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\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 \PHPStan\Rules\Rule<\PHPStan\Node\MethodReturnStatementsNode> + * @implements Rule */ -class TooWideMethodReturnTypehintRule implements Rule +final class TooWideMethodReturnTypehintRule implements Rule { - private bool $checkProtectedAndPublicMethods; - - public function __construct(bool $checkProtectedAndPublicMethods) + public function __construct(private bool $checkProtectedAndPublicMethods) { - $this->checkProtectedAndPublicMethods = $checkProtectedAndPublicMethods; } public function getNodeType(): string @@ -35,21 +32,25 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - throw new \PHPStan\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,21 +68,22 @@ 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) - && !$isFirstDeclaration + !$isFirstDeclaration + && !$method->isPrivate() + && ($returnType->isNull()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) ) { return []; } @@ -92,12 +94,22 @@ public function processNode(Node $node, Scope $scope): array continue; } + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + continue 2; + } + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() never returns %s so it can be removed from the return typehint.', + 'Method %s::%s() never returns %s so it can be removed from the return type.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $type->describe(VerbosityLevel::typeOnly()) - ))->build(); + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + ))->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 80d539911a..68c1549091 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -3,29 +3,48 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +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, + private bool $reportExactLine, + ) + { + } + /** - * @param \PHPStan\Analyser\Scope $scope - * @param string[] $parameterNames - * @param \PhpParser\Node[] $statements - * @param string $unusedParameterMessage - * @param string $identifier - * @return RuleError[] + * @param Variable[] $parameterVars + * @param Node[] $statements + * @param 'constructor.unusedParameter'|'closure.unusedUse' $identifier + * @return list */ public function getUnusedParameters( Scope $scope, - array $parameterNames, + array $parameterVars, array $statements, string $unusedParameterMessage, - string $identifier + string $identifier, ): 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; @@ -34,28 +53,35 @@ public function getUnusedParameters( unset($unusedParameters[$variableName]); } $errors = []; - foreach (array_keys($unusedParameters) as $name) { - $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name) - )->identifier($identifier)->metadata(['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 \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node + * @param Node[]|Node|scalar|null $node * @return string[] */ private function getUsedVariables(Scope $scope, $node): array { $variableNames = []; if ($node instanceof Node) { - if ($node instanceof Node\Expr\Variable && is_string($node->name) && $node->name !== 'this') { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if ($functionName === 'func_get_args' || $functionName === 'get_defined_vars') { + return $scope->getDefinedVariables(); + } + } + 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 ( @@ -63,13 +89,11 @@ private function getUsedVariables(Scope $scope, $node): array && $node->name instanceof Node\Name && (string) $node->name === 'compact' ) { - foreach ($node->args as $arg) { + foreach ($node->getArgs() as $arg) { $argType = $scope->getType($arg->value); - if (!($argType instanceof ConstantStringType)) { - continue; + foreach ($argType->getConstantStrings() as $constantStringType) { + $variableNames[] = $constantStringType->getValue(); } - - $variableNames[] = $argType->getValue(); } } foreach ($node->getSubNodeNames() as $subNodeName) { diff --git a/src/Rules/Variables/CompactVariablesRule.php b/src/Rules/Variables/CompactVariablesRule.php index 6099d7b3e0..8555675bd3 100644 --- a/src/Rules/Variables/CompactVariablesRule.php +++ b/src/Rules/Variables/CompactVariablesRule.php @@ -7,18 +7,20 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Type; +use function array_merge; +use function count; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ final class CompactVariablesRule implements Rule { - private bool $checkMaybeUndefinedVariables; - - public function __construct(bool $checkMaybeUndefinedVariables) + public function __construct(private bool $checkMaybeUndefinedVariables) { - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function getNodeType(): string @@ -38,30 +40,50 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionArguments = $node->args; + $functionArguments = $node->getArgs(); $messages = []; foreach ($functionArguments as $argument) { $argumentType = $scope->getType($argument->value); - if (!$argumentType instanceof ConstantStringType) { - continue; - } + $constantStrings = $this->findConstantStrings($argumentType); + foreach ($constantStrings as $constantString) { + $variableName = $constantString->getValue(); + $scopeHasVariable = $scope->hasVariableType($variableName); - $variableName = $argumentType->getValue(); - $scopeHasVariable = $scope->hasVariableType($variableName); - - if ($scopeHasVariable->no()) { - $messages[] = RuleErrorBuilder::message( - sprintf('Call to function compact() contains undefined variable $%s.', $variableName) - )->line($argument->getLine())->build(); - } elseif ($this->checkMaybeUndefinedVariables && $scopeHasVariable->maybe()) { - $messages[] = RuleErrorBuilder::message( - sprintf('Call to function compact() contains possibly undefined variable $%s.', $variableName) - )->line($argument->getLine())->build(); + if ($scopeHasVariable->no()) { + $messages[] = RuleErrorBuilder::message( + sprintf('Call to function compact() contains undefined variable $%s.', $variableName), + )->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), + )->identifier('variable.undefined')->line($argument->getStartLine())->build(); + } } } return $messages; } + /** + * @return list + */ + private function findConstantStrings(Type $type): array + { + $constantStrings = $type->getConstantStrings(); + if (count($constantStrings) > 0) { + return $constantStrings; + } + + $result = []; + foreach ($type->getConstantArrays() as $constantArrayType) { + foreach ($constantArrayType->getValueTypes() as $valueType) { + $constantStrings = $this->findConstantStrings($valueType); + $result = array_merge($result, $constantStrings); + } + } + + return $result; + } + } diff --git a/src/Rules/Variables/DefinedVariableInAnonymousFunctionUseRule.php b/src/Rules/Variables/DefinedVariableInAnonymousFunctionUseRule.php deleted file mode 100644 index 6e52db9183..0000000000 --- a/src/Rules/Variables/DefinedVariableInAnonymousFunctionUseRule.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -class DefinedVariableInAnonymousFunctionUseRule implements \PHPStan\Rules\Rule -{ - - private bool $checkMaybeUndefinedVariables; - - public function __construct( - bool $checkMaybeUndefinedVariables - ) - { - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; - } - - public function getNodeType(): string - { - return ClosureUse::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($node->byRef || !is_string($node->var->name)) { - return []; - } - - if ($scope->hasVariableType($node->var->name)->no()) { - return [ - RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $node->var->name))->build(), - ]; - } elseif ( - $this->checkMaybeUndefinedVariables - && !$scope->hasVariableType($node->var->name)->yes() - ) { - return [ - RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $node->var->name))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index 9585dabcef..2056a89dbe 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -3,27 +3,29 @@ 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; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Variable> + * @implements Rule */ -class DefinedVariableRule implements \PHPStan\Rules\Rule +final class DefinedVariableRule implements Rule { - private bool $cliArgumentsVariablesRegistered; - - private bool $checkMaybeUndefinedVariables; - public function __construct( - bool $cliArgumentsVariablesRegistered, - bool $checkMaybeUndefinedVariables + private bool $cliArgumentsVariablesRegistered, + private bool $checkMaybeUndefinedVariables, ) { - $this->cliArgumentsVariablesRegistered = $cliArgumentsVariablesRegistered; - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function getNodeType(): string @@ -33,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)) { @@ -47,20 +73,24 @@ 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))->build(), + RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $variableName)) + ->identifier('variable.undefined') + ->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))->build(), + RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $variableName)) + ->identifier('variable.undefined') + ->build(), ]; } diff --git a/src/Rules/Variables/EmptyRule.php b/src/Rules/Variables/EmptyRule.php new file mode 100644 index 0000000000..12d3fadf59 --- /dev/null +++ b/src/Rules/Variables/EmptyRule.php @@ -0,0 +1,67 @@ + + */ +final class EmptyRule implements Rule +{ + + public function __construct(private IssetCheck $issetCheck) + { + } + + public function getNodeType(): string + { + return Node\Expr\Empty_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', 'empty', static function (Type $type): ?string { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + $isFalsey = $type->toBoolean()->isFalse(); + if ($isFalsey->maybe()) { + return null; + } + + if ($isNull->yes()) { + if ($isFalsey->yes()) { + return 'is always falsy'; + } + if ($isFalsey->no()) { + return 'is not falsy'; + } + + return 'is always null'; + } + + if ($isFalsey->yes()) { + return 'is always falsy'; + } + + if ($isFalsey->no()) { + return 'is not falsy'; + } + + return 'is not nullable'; + }); + + if ($error === null) { + return []; + } + + return [$error]; + } + +} diff --git a/src/Rules/Variables/IssetRule.php b/src/Rules/Variables/IssetRule.php index 78ab199e97..69ed263479 100644 --- a/src/Rules/Variables/IssetRule.php +++ b/src/Rules/Variables/IssetRule.php @@ -5,18 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; +use PHPStan\Rules\Rule; +use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class IssetRule implements \PHPStan\Rules\Rule +final class IssetRule implements Rule { - private IssetCheck $issetCheck; - - public function __construct(IssetCheck $issetCheck) + public function __construct(private IssetCheck $issetCheck) { - $this->issetCheck = $issetCheck; } public function getNodeType(): string @@ -28,7 +27,18 @@ public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->vars as $var) { - $error = $this->issetCheck->check($var, $scope, 'in isset()'); + $error = $this->issetCheck->check($var, $scope, 'in isset()', 'isset', static function (Type $type): ?string { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + if ($isNull->yes()) { + return 'is always null'; + } + + return 'is not nullable'; + }); if ($error === null) { continue; } diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index 5c6790e3f8..563bec59f7 100644 --- a/src/Rules/Variables/NullCoalesceRule.php +++ b/src/Rules/Variables/NullCoalesceRule.php @@ -5,31 +5,43 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; +use PHPStan\Rules\Rule; +use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class NullCoalesceRule implements \PHPStan\Rules\Rule +final class NullCoalesceRule implements Rule { - private IssetCheck $issetCheck; - - public function __construct(IssetCheck $issetCheck) + public function __construct(private IssetCheck $issetCheck) { - $this->issetCheck = $issetCheck; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } public function processNode(Node $node, Scope $scope): array { + $typeMessageCallback = static function (Type $type): ?string { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + if ($isNull->yes()) { + return 'is always null'; + } + + return 'is not nullable'; + }; + if ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??'); + $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', 'nullCoalesce', $typeMessageCallback); } elseif ($node instanceof Node\Expr\AssignOp\Coalesce) { - $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??='); + $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', '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/ThisVariableRule.php b/src/Rules/Variables/ThisVariableRule.php deleted file mode 100644 index 95c32ca77b..0000000000 --- a/src/Rules/Variables/ThisVariableRule.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ -class ThisVariableRule implements \PHPStan\Rules\Rule -{ - - public function getNodeType(): string - { - return Variable::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!is_string($node->name) || $node->name !== 'this') { - return []; - } - - if ($scope->isInClosureBind()) { - return []; - } - - if (!$scope->isInClass()) { - return [ - RuleErrorBuilder::message('Using $this outside a class.')->build(), - ]; - } - - $function = $scope->getFunction(); - if (!$function instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); - } - - if ($function->isStatic()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Using $this in static method %s::%s().', - $scope->getClassReflection()->getDisplayName(), - $function->getName() - ))->build(), - ]; - } - - return []; - } - -} diff --git a/src/Rules/Variables/ThrowTypeRule.php b/src/Rules/Variables/ThrowTypeRule.php deleted file mode 100644 index 51b4c0bbfe..0000000000 --- a/src/Rules/Variables/ThrowTypeRule.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -class ThrowTypeRule implements \PHPStan\Rules\Rule -{ - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct( - RuleLevelHelper $ruleLevelHelper - ) - { - $this->ruleLevelHelper = $ruleLevelHelper; - } - - public function getNodeType(): string - { - return \PhpParser\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 function (Type $type) use ($throwableType): bool { - return $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 58ab8457d8..91e5260488 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,16 +4,28 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Rules\RuleError; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Unset_> + * @implements Rule */ -class UnsetRule implements \PHPStan\Rules\Rule +final class UnsetRule implements Rule { + public function __construct( + private PropertyReflectionFinder $propertyReflectionFinder, + private PhpVersion $phpVersion, + ) + { + } + public function getNodeType(): string { return Node\Stmt\Unset_::class; @@ -25,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; @@ -36,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(); + sprintf('Call to function unset() contains undefined variable $%s.', $node->name), + ) + ->line($node->getStartLine()) + ->identifier('unset.variable') + ->build(); } } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { $type = $scope->getType($node->var); @@ -54,9 +130,12 @@ private function canBeUnset(Node $node, Scope $scope): ?RuleError sprintf( 'Cannot unset offset %s on %s.', $dimType->describe(VerbosityLevel::value()), - $type->describe(VerbosityLevel::value()) - ) - )->line($node->getLine())->build(); + $type->describe(VerbosityLevel::value()), + ), + ) + ->line($node->getStartLine()) + ->identifier('unset.offset') + ->build(); } return $this->canBeUnset($node->var, $scope); diff --git a/src/Rules/Variables/VariableCertaintyInIssetRule.php b/src/Rules/Variables/VariableCertaintyInIssetRule.php deleted file mode 100644 index 1674e73438..0000000000 --- a/src/Rules/Variables/VariableCertaintyInIssetRule.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ -class VariableCertaintyInIssetRule implements \PHPStan\Rules\Rule -{ - - public function getNodeType(): string - { - return Node\Expr\Isset_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $messages = []; - foreach ($node->vars as $var) { - $isSubNode = false; - while ( - $var instanceof Node\Expr\ArrayDimFetch - || $var instanceof Node\Expr\PropertyFetch - || ( - $var instanceof Node\Expr\StaticPropertyFetch - && $var->class instanceof Node\Expr - ) - ) { - if ($var instanceof Node\Expr\StaticPropertyFetch) { - $var = $var->class; - } else { - $var = $var->var; - } - $isSubNode = true; - } - - if (!$var instanceof Node\Expr\Variable || !is_string($var->name)) { - continue; - } - - $certainty = $scope->hasVariableType($var->name); - if ($certainty->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Variable $%s in isset() is never defined.', - $var->name - ))->build(); - } elseif ($certainty->yes() && !$isSubNode) { - $variableType = $scope->getVariableType($var->name); - if ($variableType->isSuperTypeOf(new NullType())->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Variable $%s in isset() always exists and is not nullable.', - $var->name - ))->build(); - } elseif ((new NullType())->isSuperTypeOf($variableType)->yes()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Variable $%s in isset() is always null.', - $var->name - ))->build(); - } - } - } - - return $messages; - } - -} diff --git a/src/Rules/Variables/VariableCertaintyNullCoalesceRule.php b/src/Rules/Variables/VariableCertaintyNullCoalesceRule.php deleted file mode 100644 index 462bd4febf..0000000000 --- a/src/Rules/Variables/VariableCertaintyNullCoalesceRule.php +++ /dev/null @@ -1,86 +0,0 @@ - - */ -class VariableCertaintyNullCoalesceRule implements \PHPStan\Rules\Rule -{ - - public function getNodeType(): string - { - return Node\Expr::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($node instanceof Node\Expr\AssignOp\Coalesce) { - $var = $node->var; - $description = '??='; - } elseif ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $var = $node->left; - $description = '??'; - } else { - return []; - } - - $isSubNode = false; - while ( - $var instanceof Node\Expr\ArrayDimFetch - || $var instanceof Node\Expr\PropertyFetch - || ( - $var instanceof Node\Expr\StaticPropertyFetch - && $var->class instanceof Node\Expr - ) - ) { - if ($var instanceof Node\Expr\StaticPropertyFetch) { - $var = $var->class; - } else { - $var = $var->var; - } - $isSubNode = true; - } - - if (!$var instanceof Node\Expr\Variable || !is_string($var->name)) { - return []; - } - - $certainty = $scope->hasVariableType($var->name); - if ($certainty->no()) { - if ( - $scope->getFunction() !== null - || $scope->isInAnonymousFunction() - ) { - return [RuleErrorBuilder::message(sprintf( - 'Variable $%s on left side of %s is never defined.', - $var->name, - $description - ))->build()]; - } - } elseif ($certainty->yes() && !$isSubNode) { - $variableType = $scope->getVariableType($var->name); - if ($variableType->isSuperTypeOf(new NullType())->no()) { - return [RuleErrorBuilder::message(sprintf( - 'Variable $%s on left side of %s always exists and is not nullable.', - $var->name, - $description - ))->build()]; - } elseif ((new NullType())->isSuperTypeOf($variableType)->yes()) { - return [RuleErrorBuilder::message(sprintf( - 'Variable $%s on left side of %s is always null.', - $var->name, - $description - ))->build()]; - } - } - - return []; - } - -} diff --git a/src/Rules/Variables/VariableCloningRule.php b/src/Rules/Variables/VariableCloningRule.php index 5caa3587f4..da72e99ebe 100644 --- a/src/Rules/Variables/VariableCloningRule.php +++ b/src/Rules/Variables/VariableCloningRule.php @@ -6,23 +6,23 @@ use PhpParser\Node\Expr\Clone_; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Clone_> + * @implements Rule */ -class VariableCloningRule implements \PHPStan\Rules\Rule +final class VariableCloningRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -36,9 +36,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->expr, 'Cloning object of an unknown class %s.', - static function (Type $type): bool { - return $type->isCloneable()->yes(); - } + static fn (Type $type): bool => $type->isCloneable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -53,16 +51,16 @@ static function (Type $type): bool { RuleErrorBuilder::message(sprintf( 'Cannot clone non-object variable $%s of type %s.', $node->expr->name, - $type->describe(VerbosityLevel::typeOnly()) - ))->build(), + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('clone.nonObject')->build(), ]; } return [ RuleErrorBuilder::message(sprintf( 'Cannot clone %s.', - $type->describe(VerbosityLevel::typeOnly()) - ))->build(), + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('clone.nonObject')->build(), ]; } diff --git a/src/Rules/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php new file mode 100644 index 0000000000..3fb3cbf239 --- /dev/null +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -0,0 +1,95 @@ + + */ +final class FileWhitespaceRule 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]; + $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.') + ->identifier('whitespace.bom') + ->build(); + } + + $nodeTraverser = new NodeTraverser(); + $visitor = new class () extends NodeVisitorAbstract { + + /** @var Node[] */ + private array $lastNodes = []; + + /** + * @return int|null + */ + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\Declare_) { + if ($node->stmts !== null && count($node->stmts) > 0) { + $this->lastNodes[] = $node->stmts[count($node->stmts) - 1]; + } + return null; + } + if ($node instanceof Node\Stmt\Namespace_) { + if (count($node->stmts) > 0) { + $this->lastNodes[] = $node->stmts[count($node->stmts) - 1]; + } + return null; + } + return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + } + + /** + * @return Node[] + */ + public function getLastNodes(): array + { + return $this->lastNodes; + } + + }; + $nodeTraverser->addVisitor($visitor); + $nodeTraverser->traverse($nodes); + + $lastNodes = $visitor->getLastNodes(); + $lastNodes[] = $nodes[count($nodes) - 1]; + foreach ($lastNodes as $lastNode) { + if (!$lastNode instanceof Node\Stmt\InlineHTML || Strings::match($lastNode->value, '#^(\s+)$#') === null) { + 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()) + ->identifier('whitespace.fileEnd') + ->build(); + } + + return $messages; + } + +} diff --git a/src/ShouldNotHappenException.php b/src/ShouldNotHappenException.php index 99575886db..4f597f4b32 100644 --- a/src/ShouldNotHappenException.php +++ b/src/ShouldNotHappenException.php @@ -2,9 +2,12 @@ namespace PHPStan; -final class ShouldNotHappenException extends \Exception +use Exception; + +final class ShouldNotHappenException extends Exception { + /** @api */ public function __construct(string $message = 'Internal error.') { parent::__construct($message); 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 43859bdf4f..1009a53a0c 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -8,86 +8,130 @@ use PHPStan\Command\Output; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\StreamOutput; - -abstract class ErrorFormatterTestCase extends \PHPStan\Testing\TestCase +use function array_map; +use function array_slice; +use function explode; +use function fopen; +use function implode; +use function in_array; +use function is_int; +use function range; +use function rewind; +use function rtrim; +use function stream_get_contents; + +abstract class ErrorFormatterTestCase extends PHPStanTestCase { protected const DIRECTORY_PATH = '/data/folder/with space/and unicode 😃/project'; - 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'; + + /** @var array */ + private array $outputStream = []; - private ?Output $output = null; + /** @var array */ + private array $output = []; - private function getOutputStream(): StreamOutput + 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $this->outputStream = new StreamOutput($resource); + $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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } 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 > 4 || $numFileErrors < 0 || $numGenericErrors > 2 || $numGenericErrors < 0) { - throw new \Exception(); + 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', self::DIRECTORY_PATH . '/foo.php', 5), - new Error('Bar', self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 2), - ], 0, $numFileErrors); + 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), + 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( $fileErrors, $genericErrors, [], + [], + [], false, null, - false + true, + 0, + false, + [], ); } private function rtrimMultiline(string $output): string { - $result = array_map(static function (string $line): string { - return rtrim($line, " \r\n"); - }, explode("\n", $output)); + $result = array_map(static fn (string $line): string => rtrim($line, " \r\n"), explode("\n", $output)); return implode("\n", $result); } diff --git a/src/Testing/LevelsTestCase.php b/src/Testing/LevelsTestCase.php index c93433d475..499277dc8b 100644 --- a/src/Testing/LevelsTestCase.php +++ b/src/Testing/LevelsTestCase.php @@ -2,16 +2,35 @@ namespace PHPStan\Testing; +use Nette\Utils\Json; +use Nette\Utils\JsonException; use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; - -abstract class LevelsTestCase extends \PHPUnit\Framework\TestCase +use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\TestCase; +use function array_merge; +use function count; +use function escapeshellarg; +use function escapeshellcmd; +use function exec; +use function implode; +use function method_exists; +use function putenv; +use function range; +use function sprintf; +use function unlink; +use const DIRECTORY_SEPARATOR; +use const PHP_BINARY; + +/** @api */ +abstract class LevelsTestCase extends TestCase { /** * @return array> */ - abstract public function dataTopics(): array; + abstract public static function dataTopics(): array; abstract public function getDataPath(): string; @@ -24,12 +43,16 @@ protected function getResultSuffix(): string return ''; } + protected function shouldAutoloadAnalysedFile(): bool + { + return true; + } + /** * @dataProvider dataTopics - * @param string $topic */ public function testLevels( - string $topic + string $topic, ): void { $file = sprintf('%s' . DIRECTORY_SEPARATOR . '%s.php', $this->getDataPath(), $topic); @@ -41,20 +64,23 @@ public function testLevels( $exceptions = []; - foreach (range(0, 8) 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 \PHPStan\ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); - } - exec(sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s --autoload-file %s %s', escapeshellarg(PHP_BINARY), $command, $level, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : '', escapeshellarg($file), escapeshellarg($file)), $outputLines); + exec(sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s %s %s', escapeshellarg(PHP_BINARY), $command, $level, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : '', $this->shouldAutoloadAnalysedFile() ? sprintf('--autoload-file %s', escapeshellarg($file)) : '', escapeshellarg($file)), $outputLines); $output = implode("\n", $outputLines); try { - $actualJson = \Nette\Utils\Json::decode($output, \Nette\Utils\Json::FORCE_ARRAY); - } catch (\Nette\Utils\JsonException $e) { - throw new \Nette\Utils\JsonException(sprintf('Cannot decode: %s', $output)); + $actualJson = Json::decode($output, Json::FORCE_ARRAY); + } catch (JsonException) { + throw new JsonException(sprintf('Cannot decode: %s', $output)); } if (count($actualJson['files']) > 0) { $normalizedFilePath = $fileHelper->normalizePath($file); @@ -87,6 +113,9 @@ public function testLevels( } } + unset($message['tip']); + unset($message['identifier']); + $messages[] = $message; } @@ -101,6 +130,8 @@ public function testLevels( } } + unset($previousMessage['tip']); + $missingMessages[] = $previousMessage; } @@ -135,30 +166,28 @@ public function getAdditionalAnalysedFiles(): array } /** - * @param string $expectedJsonFile * @param string[] $expectedMessages - * @return \PHPUnit\Framework\AssertionFailedError|null */ - private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?\PHPUnit\Framework\AssertionFailedError + private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?AssertionFailedError { if (count($expectedMessages) === 0) { try { - $this->assertFileNotExists($expectedJsonFile); + self::ourCustomAssertFileDoesNotExist($expectedJsonFile); return null; - } catch (\PHPUnit\Framework\AssertionFailedError $e) { + } catch (AssertionFailedError $e) { unlink($expectedJsonFile); return $e; } } - $actualOutput = \Nette\Utils\Json::encode($expectedMessages, \Nette\Utils\Json::PRETTY); + $actualOutput = Json::encode($expectedMessages, Json::PRETTY); try { $this->assertJsonStringEqualsJsonFile( $expectedJsonFile, - $actualOutput + $actualOutput, ); - } catch (\PHPUnit\Framework\AssertionFailedError $e) { + } catch (AssertionFailedError $e) { FileWriter::write($expectedJsonFile, $actualOutput); return $e; } @@ -166,4 +195,15 @@ private function compareFiles(string $expectedJsonFile, array $expectedMessages) return null; } + 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; + } + + parent::assertFileDoesNotExist($filename, $message); + } + } 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 new file mode 100644 index 0000000000..7587ac8d05 --- /dev/null +++ b/src/Testing/PHPStanTestCase.php @@ -0,0 +1,250 @@ + */ + private static array $containers = []; + + /** @api */ + public static function getContainer(): Container + { + $additionalConfigFiles = static::getAdditionalConfigFiles(); + $additionalConfigFiles[] = __DIR__ . '/TestCase.neon'; + $cacheKey = sha1(implode("\n", $additionalConfigFiles)); + + if (!isset(self::$containers[$cacheKey])) { + $tmpDir = sys_get_temp_dir() . '/phpstan-tests'; + try { + DirectoryCreator::ensureDirectoryExists($tmpDir, 0777); + } catch (DirectoryCreatorException $e) { + self::fail($e->getMessage()); + } + + $rootDir = __DIR__ . '/../..'; + $fileHelper = new FileHelper($rootDir); + $rootDir = $fileHelper->normalizePath($rootDir, '/'); + $containerFactory = new ContainerFactory($rootDir); + $container = $containerFactory->create($tmpDir, array_merge([ + $containerFactory->getConfigDirectory() . '/config.level8.neon', + ], $additionalConfigFiles), []); + self::$containers[$cacheKey] = $container; + + foreach ($container->getParameter('bootstrapFiles') as $bootstrapFile) { + (static function (string $file) use ($container): void { + require_once $file; + })($bootstrapFile); + } + + if (PHP_VERSION_ID >= 80000) { + require_once __DIR__ . '/../../stubs/runtime/Enum/UnitEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/BackedEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; + } + } else { + ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]); + } + + return self::$containers[$cacheKey]; + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return []; + } + + public static function getParser(): Parser + { + /** @var Parser $parser */ + $parser = self::getContainer()->getService('defaultAnalysisParser'); + return $parser; + } + + /** @api */ + public static function createReflectionProvider(): ReflectionProvider + { + return self::getContainer()->getByType(ReflectionProvider::class); + } + + public static function getReflector(): Reflector + { + return self::getContainer()->getService('betterReflectionReflector'); + } + + public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider + { + return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); + } + + /** + * @param string[] $dynamicConstantNames + */ + public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory + { + $container = self::getContainer(); + + 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->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 static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + { + $container = self::getContainer(); + + return new UsefulTypeAliasResolver( + $globalTypeAliases, + $container->getByType(TypeStringResolver::class), + $container->getByType(TypeNodeResolver::class), + $reflectionProvider, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return true; + } + + public static function getFileHelper(): FileHelper + { + return self::getContainer()->getByType(FileHelper::class); + } + + /** + * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths. + * + */ + protected function assertSamePaths(string $expected, string $actual, string $message = ''): void + { + $expected = $this->getFileHelper()->normalizePath($expected); + $actual = $this->getFileHelper()->normalizePath($actual); + + $this->assertSame($expected, $actual, $message); + } + + /** + * @param Error[]|string[] $errors + */ + protected function assertNoErrors(array $errors): void + { + try { + $this->assertCount(0, $errors); + } catch (ExpectationFailedException $e) { + $messages = []; + foreach ($errors as $error) { + if ($error instanceof Error) { + $messages[] = sprintf("- %s\n in %s on line %d\n", rtrim($error->getMessage(), '.'), $error->getFile(), $error->getLine()); + } else { + $messages[] = $error; + } + } + + $this->fail($e->getMessage() . "\n\nEmitted errors:\n" . implode("\n", $messages)); + } + } + + protected function skipIfNotOnWindows(): void + { + if (DIRECTORY_SEPARATOR === '\\') { + return; + } + + self::markTestSkipped(); + } + + protected function skipIfNotOnUnix(): void + { + if (DIRECTORY_SEPARATOR === '/') { + return; + } + + self::markTestSkipped(); + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 77febfddbb..026069146a 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -2,134 +2,146 @@ 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\Broker\AnonymousClassNameHelper; -use PHPStan\Cache\Cache; +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\File\SimpleRelativePathHelper; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; -use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; -use PHPStan\Rules\Registry; +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\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; /** - * @template TRule of \PHPStan\Rules\Rule + * @api + * @template TRule of Rule */ -abstract class RuleTestCase extends \PHPStan\Testing\TestCase +abstract class RuleTestCase extends PHPStanTestCase { - private ?\PHPStan\Analyser\Analyser $analyser = null; + private ?Analyser $analyser = null; /** - * @return \PHPStan\Rules\Rule - * @phpstan-return TRule + * @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 $this->createTypeSpecifier( - new \PhpParser\PrettyPrinter\Standard(), - $this->createReflectionProvider(), - $this->getMethodTypeSpecifyingExtensions(), - $this->getStaticMethodTypeSpecifyingExtensions() - ); + return self::getContainer()->getService('typeSpecifier'); } - private function getAnalyser(): Analyser + private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser { if ($this->analyser === null) { - $registry = new Registry([ - $this->getRule(), - ]); - - $broker = $this->createBroker(); - $printer = new \PhpParser\PrettyPrinter\Standard(); - $typeSpecifier = $this->createTypeSpecifier( - $printer, - $broker, - $this->getMethodTypeSpecifyingExtensions(), - $this->getStaticMethodTypeSpecifyingExtensions() - ); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $currentWorkingDirectory = $fileHelper->normalizePath($currentWorkingDirectory, '/'); - $fileHelper = new FileHelper($currentWorkingDirectory); - $relativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, $relativePathHelper); - $fileTypeMapper = new FileTypeMapper(new DirectReflectionProviderProvider($broker), $this->getParser(), self::getContainer()->getByType(PhpDocStringResolver::class), self::getContainer()->getByType(PhpDocNodeResolver::class), $this->createMock(Cache::class), $anonymousClassNameHelper); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); + $collectorRegistry = new CollectorRegistry($this->getCollectors()); + + $reflectionProvider = $this->createReflectionProvider(); + $typeSpecifier = $this->getTypeSpecifier(); + + $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); $nodeScopeResolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), + $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getReflector(), + self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), $this->getParser(), - $fileTypeMapper, - $phpDocInheritanceResolver, - $fileHelper, + 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->shouldPolluteCatchScopeWithTryAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), + self::getContainer()->getParameter('polluteScopeWithBlock'), + [], [], - [] + self::getContainer()->getParameter('universalObjectCratesClasses'), + self::getContainer()->getParameter('exceptions')['implicitThrows'], + $this->shouldTreatPhpDocTypesAsCertain(), + $this->shouldNarrowMethodScopeFromConstructor(), ); $fileAnalyser = new FileAnalyser( - $this->createScopeFactory($broker, $typeSpecifier), + $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, $this->getParser(), - new DependencyResolver($broker), - $fileHelper, - true + self::getContainer()->getByType(DependencyResolver::class), + new IgnoreErrorExtensionProvider(self::getContainer()), + new RuleErrorTransformer(), + new LocalIgnoresProcessor(), ); $this->analyser = new Analyser( $fileAnalyser, - $registry, + $ruleRegistry, + $collectorRegistry, $nodeScopeResolver, - 50 + 50, ); } return $this->analyser; } - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] - */ - protected function getStaticMethodTypeSpecifyingExtensions(): array - { - return []; - } - /** * @param string[] $files - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function analyse(array $files, array $expectedErrors): void { - $files = array_map([$this->getFileHelper(), 'normalizePath'], $files); - $analyserResult = $this->getAnalyser()->analyse($files); - 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) { @@ -140,16 +152,8 @@ public function analyse(array $files, array $expectedErrors): void }; $expectedErrors = array_map( - static function (array $error) use ($strictlyTypedSprintf): string { - if (!isset($error[0])) { - throw new \InvalidArgumentException('Missing expected error message.'); - } - if (!isset($error[1])) { - throw new \InvalidArgumentException('Missing expected file line.'); - } - return $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null); - }, - $expectedErrors + static fn (array $error): string => $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null), + $expectedErrors, ); $actualErrors = array_map( @@ -160,20 +164,94 @@ static function (Error $error) use ($strictlyTypedSprintf): string { } return $strictlyTypedSprintf($line, $error->getMessage(), $error->getTip()); }, - $actualErrors + $actualErrors, ); - $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n"); + $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); } - protected function shouldPolluteScopeWithLoopInitialAssignments(): bool + /** + * @param string[] $files + * @return list + */ + public function gatherAnalyserErrors(array $files): array { - return false; + return $this->gatherAnalyserErrorsWithDelayedErrors($files)[0]; } - protected function shouldPolluteCatchScopeWithTryAssignments(): bool + /** + * @param string[] $files + * @return array{list, list} + */ + private function gatherAnalyserErrorsWithDelayedErrors(array $files): array { - return false; + $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 true; } protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool @@ -181,4 +259,21 @@ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool return true; } + protected function shouldFailOnPhpErrors(): bool + { + return true; + } + + protected function shouldNarrowMethodScopeFromConstructor(): bool + { + return false; + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../conf/bleedingEdge.neon', + ]; + } + } diff --git a/src/Testing/TestCase-staticReflection.neon b/src/Testing/TestCase-staticReflection.neon deleted file mode 100644 index 5cf674f093..0000000000 --- a/src/Testing/TestCase-staticReflection.neon +++ /dev/null @@ -1,43 +0,0 @@ -services: - - - class: PHPStan\Testing\TestCaseSourceLocatorFactory - arguments: - phpParser: @phpParserDecorator - - testCaseBetterReflectionProvider: - class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider - arguments: - classReflector: @testCaseClassReflector - functionReflector: @testCaseFunctionReflector - constantReflector: @testCaseConstantReflector - autowired: false - - testCaseClassReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingClassReflector - arguments: - sourceLocator: @testCaseSourceLocator - autowired: false - - testCaseFunctionReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingFunctionReflector - arguments: - classReflector: @testCaseClassReflector - sourceLocator: @testCaseSourceLocator - autowired: false - - testCaseConstantReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingConstantReflector - arguments: - classReflector: @testCaseClassReflector - sourceLocator: @testCaseSourceLocator - autowired: false - - testCaseSourceLocator: - class: Roave\BetterReflection\SourceLocator\Type\SourceLocator - factory: @PHPStan\Testing\TestCaseSourceLocatorFactory::create() - autowired: false - - reflectionProvider: - factory: @testCaseBetterReflectionProvider - autowired: - - PHPStan\Reflection\ReflectionProvider diff --git a/src/Testing/TestCase.neon b/src/Testing/TestCase.neon new file mode 100644 index 0000000000..c5ef275d1f --- /dev/null +++ b/src/Testing/TestCase.neon @@ -0,0 +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/TestCase.php b/src/Testing/TestCase.php deleted file mode 100644 index b8abfd072c..0000000000 --- a/src/Testing/TestCase.php +++ /dev/null @@ -1,660 +0,0 @@ - */ - private static array $containers = []; - - private ?DirectClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider = null; - - /** @var array{ClassReflector, FunctionReflector, ConstantReflector}|null */ - private static $reflectors; - - /** @var PhpStormStubsSourceStubber|null */ - private static $phpStormStubsSourceStubber; - - public static function getContainer(): Container - { - $additionalConfigFiles = static::getAdditionalConfigFiles(); - $cacheKey = sha1(implode("\n", $additionalConfigFiles)); - - if (!isset(self::$containers[$cacheKey])) { - $tmpDir = sys_get_temp_dir() . '/phpstan-tests'; - if (!@mkdir($tmpDir, 0777) && !is_dir($tmpDir)) { - self::fail(sprintf('Cannot create temp directory %s', $tmpDir)); - } - - if (self::$useStaticReflectionProvider) { - $additionalConfigFiles[] = __DIR__ . '/TestCase-staticReflection.neon'; - } - - $rootDir = __DIR__ . '/../..'; - $containerFactory = new ContainerFactory($rootDir); - self::$containers[$cacheKey] = $containerFactory->create($tmpDir, array_merge([ - $containerFactory->getConfigDirectory() . '/config.level8.neon', - ], $additionalConfigFiles), []); - } - - return self::$containers[$cacheKey]; - } - - /** - * @return string[] - */ - public static function getAdditionalConfigFiles(): array - { - return []; - } - - public function getParser(): \PHPStan\Parser\Parser - { - /** @var \PHPStan\Parser\Parser $parser */ - $parser = self::getContainer()->getByType(CachedParser::class); - return $parser; - } - - /** - * @param \PHPStan\Type\DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @return \PHPStan\Broker\Broker - */ - public function createBroker( - array $dynamicMethodReturnTypeExtensions = [], - array $dynamicStaticMethodReturnTypeExtensions = [] - ): Broker - { - $dynamicReturnTypeExtensionRegistryProvider = new DirectDynamicReturnTypeExtensionRegistryProvider( - array_merge(self::getContainer()->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $dynamicMethodReturnTypeExtensions, $this->getDynamicMethodReturnTypeExtensions()), - array_merge(self::getContainer()->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $dynamicStaticMethodReturnTypeExtensions, $this->getDynamicStaticMethodReturnTypeExtensions()), - array_merge(self::getContainer()->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), $this->getDynamicFunctionReturnTypeExtensions()) - ); - $operatorTypeSpecifyingExtensionRegistryProvider = new DirectOperatorTypeSpecifyingExtensionRegistryProvider( - $this->getOperatorTypeSpecifyingExtensions() - ); - $reflectionProvider = $this->createReflectionProvider(); - $broker = new Broker( - $reflectionProvider, - $dynamicReturnTypeExtensionRegistryProvider, - $operatorTypeSpecifyingExtensionRegistryProvider, - self::getContainer()->getParameter('universalObjectCratesClasses') - ); - $dynamicReturnTypeExtensionRegistryProvider->setBroker($broker); - $dynamicReturnTypeExtensionRegistryProvider->setReflectionProvider($reflectionProvider); - $operatorTypeSpecifyingExtensionRegistryProvider->setBroker($broker); - $this->getClassReflectionExtensionRegistryProvider()->setBroker($broker); - - return $broker; - } - - public function createReflectionProvider(): ReflectionProvider - { - $staticReflectionProvider = $this->createStaticReflectionProvider(); - return $this->createReflectionProviderByParameters( - $this->createRuntimeReflectionProvider($staticReflectionProvider), - $staticReflectionProvider, - self::$useStaticReflectionProvider - ); - } - - private function createReflectionProviderByParameters( - ReflectionProvider $runtimeReflectionProvider, - ReflectionProvider $staticReflectionProvider, - bool $disableRuntimeReflectionProvider - ): ReflectionProvider - { - $setterReflectionProviderProvider = new ReflectionProvider\SetterReflectionProviderProvider(); - $reflectionProviderFactory = new ReflectionProviderFactory( - $runtimeReflectionProvider, - $staticReflectionProvider, - $disableRuntimeReflectionProvider - ); - $reflectionProvider = $reflectionProviderFactory->create(); - $setterReflectionProviderProvider->setReflectionProvider($reflectionProvider); - - return $reflectionProvider; - } - - private static function getPhpStormStubsSourceStubber(): PhpStormStubsSourceStubber - { - if (self::$phpStormStubsSourceStubber === null) { - self::$phpStormStubsSourceStubber = new PhpStormStubsSourceStubber(new PhpParserDecorator(self::getContainer()->getByType(CachedParser::class))); - } - - return self::$phpStormStubsSourceStubber; - } - - private function createRuntimeReflectionProvider(ReflectionProvider $actualReflectionProvider): ReflectionProvider - { - $functionCallStatementFinder = new FunctionCallStatementFinder(); - $parser = $this->getParser(); - $cache = new Cache(new MemoryCacheStorage()); - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $relativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper(new FileHelper($currentWorkingDirectory), new SimpleRelativePathHelper($fileHelper->normalizePath($currentWorkingDirectory, '/'))); - $setterReflectionProviderProvider = new ReflectionProvider\SetterReflectionProviderProvider(); - $fileTypeMapper = new FileTypeMapper($setterReflectionProviderProvider, $parser, $phpDocStringResolver, $phpDocNodeResolver, $cache, $anonymousClassNameHelper); - $classReflectionExtensionRegistryProvider = $this->getClassReflectionExtensionRegistryProvider(); - $functionReflectionFactory = $this->getFunctionReflectionFactory( - $functionCallStatementFinder, - $cache - ); - $reflectionProvider = new ClassBlacklistReflectionProvider( - new RuntimeReflectionProvider( - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $functionReflectionFactory, - $fileTypeMapper, - self::getContainer()->getByType(NativeFunctionReflectionProvider::class), - self::getContainer()->getByType(Standard::class), - $anonymousClassNameHelper, - $fileHelper, - $relativePathHelper, - self::getContainer()->getByType(StubPhpDocProvider::class) - ), - self::getPhpStormStubsSourceStubber(), - [ - '#^PhpParser\\\\#', - '#^PHPStan\\\\#', - '#^Hoa\\\\#', - ] - ); - $this->setUpReflectionProvider( - $actualReflectionProvider, - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $functionCallStatementFinder, - $parser, - $cache, - $fileTypeMapper - ); - - return $reflectionProvider; - } - - private function setUpReflectionProvider( - ReflectionProvider $actualReflectionProvider, - ReflectionProvider\SetterReflectionProviderProvider $setterReflectionProviderProvider, - DirectClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - FunctionCallStatementFinder $functionCallStatementFinder, - \PHPStan\Parser\Parser $parser, - Cache $cache, - FileTypeMapper $fileTypeMapper - ): void - { - $methodReflectionFactory = new class($parser, $functionCallStatementFinder, $cache) implements PhpMethodReflectionFactory { - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - public ReflectionProvider $reflectionProvider; - - public function __construct( - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache - ) - { - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - } - - /** - * @param ClassReflection $declaringClass - * @param ClassReflection|null $declaringTrait - * @param \PHPStan\Reflection\Php\BuiltinMethodReflection $reflection - * @param TemplateTypeMap $templateTypeMap - * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|null $stubPhpDocString - * @return PhpMethodReflection - */ - public function create( - ClassReflection $declaringClass, - ?ClassReflection $declaringTrait, - \PHPStan\Reflection\Php\BuiltinMethodReflection $reflection, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - ?string $stubPhpDocString - ): PhpMethodReflection - { - return new PhpMethodReflection( - $declaringClass, - $declaringTrait, - $reflection, - $this->reflectionProvider, - $this->parser, - $this->functionCallStatementFinder, - $this->cache, - $templateTypeMap, - $phpDocParameterTypes, - $phpDocReturnType, - $phpDocThrowType, - $deprecatedDescription, - $isDeprecated, - $isInternal, - $isFinal, - $stubPhpDocString - ); - } - - }; - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); - $annotationsMethodsClassReflectionExtension = new AnnotationsMethodsClassReflectionExtension($fileTypeMapper); - $annotationsPropertiesClassReflectionExtension = new AnnotationsPropertiesClassReflectionExtension($fileTypeMapper); - $signatureMapProvider = self::getContainer()->getByType(SignatureMapProvider::class); - $methodReflectionFactory->reflectionProvider = $actualReflectionProvider; - $phpExtension = new PhpClassReflectionExtension(self::getContainer()->getByType(ScopeFactory::class), self::getContainer()->getByType(NodeScopeResolver::class), $methodReflectionFactory, $phpDocInheritanceResolver, $annotationsMethodsClassReflectionExtension, $annotationsPropertiesClassReflectionExtension, $signatureMapProvider, $parser, self::getContainer()->getByType(StubPhpDocProvider::class), $actualReflectionProvider, true, []); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension($phpExtension); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new UniversalObjectCratesClassReflectionExtension([\stdClass::class])); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new MixinPropertiesClassReflectionExtension([])); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new SimpleXMLElementClassPropertyReflectionExtension()); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension($annotationsPropertiesClassReflectionExtension); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension($phpExtension); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension(new MixinMethodsClassReflectionExtension([])); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension($annotationsMethodsClassReflectionExtension); - - $setterReflectionProviderProvider->setReflectionProvider($actualReflectionProvider); - } - - private function createStaticReflectionProvider(): ReflectionProvider - { - $parser = $this->getParser(); - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $cache = new Cache(new MemoryCacheStorage()); - $fileHelper = new FileHelper($currentWorkingDirectory); - $relativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, new SimpleRelativePathHelper($fileHelper->normalizePath($currentWorkingDirectory, '/'))); - $setterReflectionProviderProvider = new ReflectionProvider\SetterReflectionProviderProvider(); - $fileTypeMapper = new FileTypeMapper($setterReflectionProviderProvider, $parser, $phpDocStringResolver, $phpDocNodeResolver, $cache, $anonymousClassNameHelper); - $functionCallStatementFinder = new FunctionCallStatementFinder(); - $functionReflectionFactory = $this->getFunctionReflectionFactory( - $functionCallStatementFinder, - $cache - ); - - [$classReflector, $functionReflector, $constantReflector] = self::getReflectors(); - - $classReflectionExtensionRegistryProvider = $this->getClassReflectionExtensionRegistryProvider(); - - $reflectionProvider = new BetterReflectionProvider( - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $classReflector, - $fileTypeMapper, - self::getContainer()->getByType(NativeFunctionReflectionProvider::class), - self::getContainer()->getByType(StubPhpDocProvider::class), - $functionReflectionFactory, - $relativePathHelper, - $anonymousClassNameHelper, - self::getContainer()->getByType(Standard::class), - $fileHelper, - $functionReflector, - $constantReflector - ); - - $this->setUpReflectionProvider( - $reflectionProvider, - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $functionCallStatementFinder, - $parser, - $cache, - $fileTypeMapper - ); - - return $reflectionProvider; - } - - /** - * @return array{ClassReflector, FunctionReflector, ConstantReflector} - */ - public static function getReflectors(): array - { - if (self::$reflectors !== null) { - return self::$reflectors; - } - - if (!class_exists(ClassLoader::class)) { - self::fail('Composer ClassLoader is unknown'); - } - - $classLoaderReflection = new \ReflectionClass(ClassLoader::class); - if ($classLoaderReflection->getFileName() === false) { - self::fail('Unknown ClassLoader filename'); - } - - $composerProjectPath = dirname($classLoaderReflection->getFileName(), 3); - if (!is_file($composerProjectPath . '/composer.json')) { - self::fail(sprintf('composer.json not found in directory %s', $composerProjectPath)); - } - - $composerJsonAndInstalledJsonSourceLocatorMaker = self::getContainer()->getByType(ComposerJsonAndInstalledJsonSourceLocatorMaker::class); - $composerSourceLocator = $composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); - if ($composerSourceLocator === null) { - self::fail('Could not create composer source locator'); - } - - // these need to be synced with TestCase-staticReflection.neon file and TestCaseSourceLocatorFactory - - $locators = [ - $composerSourceLocator, - ]; - - $phpParser = new PhpParserDecorator(self::getContainer()->getByType(CachedParser::class)); - - /** @var FunctionReflector $functionReflector */ - $functionReflector = null; - $astLocator = new Locator($phpParser, static function () use (&$functionReflector): FunctionReflector { - return $functionReflector; - }); - $reflectionSourceStubber = new ReflectionSourceStubber(); - $locators[] = new PhpInternalSourceLocator($astLocator, self::getPhpStormStubsSourceStubber()); - $locators[] = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class)); - $locators[] = new PhpInternalSourceLocator($astLocator, $reflectionSourceStubber); - $locators[] = new EvaledCodeSourceLocator($astLocator, $reflectionSourceStubber); - $sourceLocator = new MemoizingSourceLocator(new AggregateSourceLocator($locators)); - - $classReflector = new MemoizingClassReflector($sourceLocator); - $functionReflector = new MemoizingFunctionReflector($sourceLocator, $classReflector); - $constantReflector = new MemoizingConstantReflector($sourceLocator, $classReflector); - - self::$reflectors = [$classReflector, $functionReflector, $constantReflector]; - - return self::$reflectors; - } - - private function getFunctionReflectionFactory( - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache - ): FunctionReflectionFactory - { - return new class($this->getParser(), $functionCallStatementFinder, $cache) implements FunctionReflectionFactory { - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - public function __construct( - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache - ) - { - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - } - - /** - * @param \ReflectionFunction $function - * @param TemplateTypeMap $templateTypeMap - * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|false $filename - * @return PhpFunctionReflection - */ - public function create( - \ReflectionFunction $function, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - $filename - ): PhpFunctionReflection - { - return new PhpFunctionReflection( - $function, - $this->parser, - $this->functionCallStatementFinder, - $this->cache, - $templateTypeMap, - $phpDocParameterTypes, - $phpDocReturnType, - $phpDocThrowType, - $deprecatedDescription, - $isDeprecated, - $isInternal, - $isFinal, - $filename - ); - } - - }; - } - - public function getClassReflectionExtensionRegistryProvider(): DirectClassReflectionExtensionRegistryProvider - { - if ($this->classReflectionExtensionRegistryProvider === null) { - $this->classReflectionExtensionRegistryProvider = new DirectClassReflectionExtensionRegistryProvider([], []); - } - - return $this->classReflectionExtensionRegistryProvider; - } - - public function createScopeFactory(Broker $broker, TypeSpecifier $typeSpecifier): ScopeFactory - { - $container = self::getContainer(); - - return new DirectScopeFactory( - MutatingScope::class, - $broker, - $broker->getDynamicReturnTypeExtensionRegistryProvider(), - $broker->getOperatorTypeSpecifyingExtensionRegistryProvider(), - new \PhpParser\PrettyPrinter\Standard(), - $typeSpecifier, - new PropertyReflectionFinder(), - $this->getParser(), - $this->shouldTreatPhpDocTypesAsCertain(), - $container - ); - } - - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return true; - } - - public function getCurrentWorkingDirectory(): string - { - return $this->getFileHelper()->normalizePath(__DIR__ . '/../..'); - } - - /** - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] - */ - public function getDynamicMethodReturnTypeExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] - */ - public function getDynamicStaticMethodReturnTypeExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] - */ - public function getDynamicFunctionReturnTypeExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\OperatorTypeSpecifyingExtension[] - */ - public function getOperatorTypeSpecifyingExtensions(): array - { - return []; - } - - /** - * @param \PhpParser\PrettyPrinter\Standard $printer - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions - * @return \PHPStan\Analyser\TypeSpecifier - */ - public function createTypeSpecifier( - Standard $printer, - ReflectionProvider $reflectionProvider, - array $methodTypeSpecifyingExtensions = [], - array $staticMethodTypeSpecifyingExtensions = [] - ): TypeSpecifier - { - return new TypeSpecifier( - $printer, - $reflectionProvider, - self::getContainer()->getServicesByTag(TypeSpecifierFactory::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG), - $methodTypeSpecifyingExtensions, - $staticMethodTypeSpecifyingExtensions - ); - } - - public function getFileHelper(): FileHelper - { - return self::getContainer()->getByType(FileHelper::class); - } - - /** - * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths. - * - * @param string $expected - * @param string $actual - * @param string $message - */ - protected function assertSamePaths(string $expected, string $actual, string $message = ''): void - { - $expected = $this->getFileHelper()->normalizePath($expected); - $actual = $this->getFileHelper()->normalizePath($actual); - - $this->assertSame($expected, $actual, $message); - } - - protected function skipIfNotOnWindows(): void - { - if (DIRECTORY_SEPARATOR === '\\') { - return; - } - - self::markTestSkipped(); - } - - protected function skipIfNotOnUnix(): void - { - if (DIRECTORY_SEPARATOR === '/') { - return; - } - - self::markTestSkipped(); - } - -} diff --git a/src/Testing/TestCaseSourceLocatorFactory.php b/src/Testing/TestCaseSourceLocatorFactory.php index d25cbc1c77..724380955e 100644 --- a/src/Testing/TestCaseSourceLocatorFactory.php +++ b/src/Testing/TestCaseSourceLocatorFactory.php @@ -3,79 +3,87 @@ namespace PHPStan\Testing; use Composer\Autoload\ClassLoader; -use PHPStan\DependencyInjection\Container; +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\Type\AggregateSourceLocator; +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\Php\PhpVersion; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; -use Roave\BetterReflection\Reflector\FunctionReflector; -use Roave\BetterReflection\SourceLocator\Ast\Locator; -use Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; -use Roave\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; -use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\EvaledCodeSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - -class TestCaseSourceLocatorFactory +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; + +final class TestCaseSourceLocatorFactory { - private Container $container; - - private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker; - - private AutoloadSourceLocator $autoloadSourceLocator; - - private \PhpParser\Parser $phpParser; - - private PhpStormStubsSourceStubber $phpstormStubsSourceStubber; - - private ReflectionSourceStubber $reflectionSourceStubber; + /** @var array> */ + private static array $composerSourceLocatorsCache = []; + /** + * @param string[] $fileExtensions + * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths + */ public function __construct( - Container $container, - ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, - AutoloadSourceLocator $autoloadSourceLocator, - \PhpParser\Parser $phpParser, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber, - ReflectionSourceStubber $reflectionSourceStubber + private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, + private Parser $phpParser, + private Parser $php8Parser, + private FileNodesFetcher $fileNodesFetcher, + private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private ReflectionSourceStubber $reflectionSourceStubber, + private PhpVersion $phpVersion, + private array $fileExtensions, + private ?array $excludePaths, ) { - $this->container = $container; - $this->composerJsonAndInstalledJsonSourceLocatorMaker = $composerJsonAndInstalledJsonSourceLocatorMaker; - $this->autoloadSourceLocator = $autoloadSourceLocator; - $this->phpParser = $phpParser; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; - $this->reflectionSourceStubber = $reflectionSourceStubber; } public function create(): SourceLocator { - $classLoaderReflection = new \ReflectionClass(ClassLoader::class); - if ($classLoaderReflection->getFileName() === false) { - throw new \PHPStan\ShouldNotHappenException('Unknown ClassLoader filename'); - } - - $composerProjectPath = dirname($classLoaderReflection->getFileName(), 3); - if (!is_file($composerProjectPath . '/composer.json')) { - throw new \PHPStan\ShouldNotHappenException(sprintf('composer.json not found in directory %s', $composerProjectPath)); - } - - $composerSourceLocator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); - if ($composerSourceLocator === null) { - throw new \PHPStan\ShouldNotHappenException('Could not create composer source locator'); + $classLoaders = ClassLoader::getRegisteredLoaders(); + $classLoaderReflection = new ReflectionClass(ClassLoader::class); + $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) { + $composerProjectPath = dirname($vendorDirProperty->getValue($classLoader)); + if (!is_file($composerProjectPath . '/composer.json')) { + continue; + } + + $composerSourceLocator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); + if ($composerSourceLocator === null) { + continue; + } + $composerLocators[] = $composerSourceLocator; + } + + self::$composerSourceLocatorsCache[$cacheKey] = $composerLocators; } - $locators = [ - $composerSourceLocator, - ]; - $astLocator = new Locator($this->phpParser, function (): FunctionReflector { - return $this->container->getService('testCaseFunctionReflector'); - }); + $locators = self::$composerSourceLocatorsCache[$cacheKey] ?? []; + $astLocator = new Locator($this->phpParser); + $astPhp8Locator = new Locator($this->php8Parser); - $locators[] = new PhpInternalSourceLocator($astLocator, $this->phpstormStubsSourceStubber); - $locators[] = $this->autoloadSourceLocator; - $locators[] = new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber); - $locators[] = new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber); + $locators[] = 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); return new MemoizingSourceLocator(new AggregateSourceLocator($locators)); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php new file mode 100644 index 0000000000..0d7e0306d1 --- /dev/null +++ b/src/Testing/TypeInferenceTestCase.php @@ -0,0 +1,427 @@ +getService('typeSpecifier'); + $fileHelper = self::getContainer()->getByType(FileHelper::class); + $resolver = new NodeScopeResolver( + $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getReflector(), + 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), + 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], static::getAdditionalAnalysedFiles()))); + + $scopeFactory = self::createScopeFactory($reflectionProvider, $typeSpecifier, $dynamicConstantNames); + $scope = $scopeFactory->create(ScopeContext::create($file)); + + $resolver->processNodes( + self::getParser()->parseFile($file), + $scope, + $callback, + ); + } + + /** + * @api + * @param mixed ...$args + */ + public function assertFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + if ($assertType === 'type') { + 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, + $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), + $failureMessage, + ); + } + } + + /** + * @api + * @return 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 = []; + $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; + } + + $nameNode = $node->name; + if (!$nameNode instanceof Name) { + return; + } + + $functionName = $nameNode->toString(); + 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->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; + } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { + $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given 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) { + self::fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); + } + if (!$certainty->class instanceof Node\Name) { + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + } + + if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') { + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + } + + if (!$certainty->name instanceof Node\Identifier) { + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + } + + // @phpstan-ignore staticMethod.dynamicName + $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); + $variable = $node->getArgs()[1]->value; + 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.')); + } + + $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variableDescription, $node->getStartLine()]; + } else { + $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) { + self::fail(sprintf( + 'ERROR: Wrong %s() call in %s on line %d.', + $functionName, + $relativePathHelper->getRelativePath($file), + $node->getStartLine(), + )); + } + + $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 = $value; } public static function createYes(): self { - return self::create(self::YES); + return self::$registry[self::YES] ??= new self(self::YES); } public static function createNo(): self { - return self::create(self::NO); + return self::$registry[self::NO] ??= new self(self::NO); } public static function createMaybe(): self { - return self::create(self::MAYBE); + return self::$registry[self::MAYBE] ??= new self(self::MAYBE); } public static function createFromBoolean(bool $value): self { - return self::create($value ? self::YES : self::NO); + $yesNo = $value ? self::YES : self::NO; + return self::$registry[$yesNo] ??= new self($yesNo); } private static function create(int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -77,9 +79,42 @@ public function toBooleanType(): BooleanType public function and(self ...$operands): self { - $operandValues = array_column($operands, 'value'); - $operandValues[] = $this->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 @@ -89,18 +124,105 @@ 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 === []) { + throw new ShouldNotHappenException(); + } $operandValues = array_column($operands, 'value'); $min = min($operandValues); $max = max($operandValues); 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 @@ -135,13 +257,4 @@ public function describe(): string return $labels[$this->value]; } - /** - * @param mixed[] $properties - * @return self - */ - 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 new file mode 100644 index 0000000000..8bcc663327 --- /dev/null +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -0,0 +1,379 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isLiteralString(), []); + } + + 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->isLiteralString(), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isLiteralString(), [])) + ->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 'literal-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->isLiteralString()->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::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 function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('literal-string'); + } + +} diff --git a/src/Type/Accessory/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 new file mode 100644 index 0000000000..f9fce63d94 --- /dev/null +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -0,0 +1,388 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isNonEmptyString(), []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + if ($type->isNonFalsyString()->yes()) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isNonEmptyString(), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isNonEmptyString(), [])) + ->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-empty-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(); + } + + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $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; + } + + 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 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::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 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 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 new file mode 100644 index 0000000000..c006d9b84b --- /dev/null +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -0,0 +1,396 @@ +isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($type->isNumericString(), []); + } + + 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->isNumericString(), []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return (new IsSuperTypeOfResult($otherType->isNumericString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + 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 + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'numeric-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; + } + + return $this; + } + + 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 UnionType([ + $this->toInteger(), + $this->toFloat(), + ]); + } + + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + [1], + [], + TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return new UnionType([ + new IntegerType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + ]); + } + + 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::createYes(); + } + + 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 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 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 4542b1984c..94364bb21f 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -2,29 +2,44 @@ 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; use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; +use function strtolower; class HasMethodType implements AccessoryType, CompoundType { use ObjectTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - private string $methodName; - - public function __construct(string $methodName) + /** @api */ + public function __construct(private string $methodName) { - $this->methodName = $methodName; } public function getReferencedClasses(): array @@ -32,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 @@ -73,11 +106,16 @@ public function equals(Type $type): bool && $this->getCanonicalMethodName() === $type->getCanonicalMethodName(); } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasMethod(%s)', $this->methodName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasMethod(string $methodName): TrinaryLogic { if ($this->getCanonicalMethodName() === strtolower($methodName)) { @@ -87,9 +125,20 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - return new DummyMethodReflection($this->methodName); + $method = new DummyMethodReflection($this->methodName); + return new CallbackUnresolvedMethodPrototypeReflection( + $method, + $method->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function isCallable(): TrinaryLogic @@ -101,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 [ @@ -108,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 8a2d7c4e2a..455f0de86e 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -2,37 +2,59 @@ 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\CompoundTypeHelper; -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; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; class HasOffsetType implements CompoundType, AccessoryType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - private \PHPStan\Type\Type $offsetType; - - public function __construct(Type $offsetType) + /** + * @api + */ + public function __construct(private ConstantStringType|ConstantIntegerType $offsetType) { - $this->offsetType = $offsetType; } + /** + * @return ConstantStringType|ConstantIntegerType + */ public function getOffsetType(): Type { return $this->offsetType; @@ -43,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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + 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 @@ -84,7 +120,7 @@ public function equals(Type $type): bool && $this->offsetType->equals($type->offsetType); } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasOffset(%s)', $this->offsetType->describe($level)); } @@ -94,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(); } @@ -108,26 +149,220 @@ public function getOffsetValueType(Type $offsetType): Type return new MixedType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + 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 + { + if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { + return new ErrorType(); + } + 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(); + } + + 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 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(); @@ -148,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 460f58298c..71b6b42759 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -2,70 +2,97 @@ 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; use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; class HasPropertyType implements AccessoryType, CompoundType { use ObjectTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - private string $propertyName; - - public function __construct(string $propertyName) + /** @api */ + public function __construct(private string $propertyName) { - $this->propertyName = $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 @@ -74,11 +101,16 @@ public function equals(Type $type): bool && $this->propertyName === $type->propertyName; } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasProperty(%s)', $this->propertyName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { @@ -93,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 6a0b9c1a5f..2fbea580ca 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -2,19 +2,31 @@ 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\ArrayType; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; +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; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; class NonEmptyArrayType implements CompoundType, AccessoryType { @@ -23,49 +35,83 @@ class NonEmptyArrayType implements CompoundType, AccessoryType use NonObjectTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + public function __construct() + { + } 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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return (new ArrayType(new MixedType(), new MixedType())) - ->isSuperTypeOf($type) - ->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(); } - return (new ArrayType(new MixedType(), new MixedType())) - ->isSuperTypeOf($type) - ->and($type->isIterableAtLeastOnce()); + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + 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 (new ArrayType(new MixedType(), new MixedType())) - ->isSuperTypeOf($otherType) - ->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 @@ -73,9 +119,9 @@ public function equals(Type $type): bool return $type instanceof self; } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { - return 'nonEmpty'; + return 'non-empty-array'; } public function isOffsetAccessible(): TrinaryLogic @@ -83,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(); @@ -93,11 +144,85 @@ public function getOffsetValueType(Type $offsetType): Type return new MixedType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + 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 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(); @@ -108,34 +233,198 @@ 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(); + } + + 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 + { + if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); } - public function toInteger(): Type + public function toAbsoluteNumber(): Type { return new ErrorType(); } + public function toInteger(): Type + { + return new ConstantIntegerType(1); + } + public function toFloat(): Type { - return new ErrorType(); + return new ConstantFloatType(1.0); } public function toString(): Type @@ -145,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 @@ -153,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 d3683f0d8d..e6c0097db7 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,38 +2,61 @@ 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\TrivialParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +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 count; +use function sprintf; +/** @api */ class ArrayType implements Type { + use ArrayTypeTrait; use MaybeCallableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; + use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; - private \PHPStan\Type\Type $keyType; + private Type $keyType; - private \PHPStan\Type\Type $itemType; - - public function __construct(Type $keyType, Type $itemType) + /** @api */ + 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; - $this->itemType = $itemType; } public function getKeyType(): Type @@ -46,30 +69,34 @@ public function getItemType(): Type return $this->itemType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return array_merge( $this->keyType->getReferencedClasses(), - $this->getItemType()->getReferencedClasses() + $this->getItemType()->getReferencedClasses(), ); } - 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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + 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; @@ -80,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) { @@ -135,33 +161,33 @@ function () use ($level, $isMixedKeyType, $isMixedItemType): string { } return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level)); - } + }, ); } public function generalizeValues(): self { - return new self($this->keyType, TypeUtils::generalizeType($this->itemType)); + return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } - public function getKeysArray(): self + 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 @@ -170,29 +196,73 @@ public function getIterableKeyType(): Type if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) { return new BenevolentUnionType([new IntegerType(), new StringType()]); } + if ($keyType instanceof StrictMixedType) { + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } return $keyType; } + 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 $this->getItemType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getItemType(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isList(): TrinaryLogic + { + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantValue(): TrinaryLogic { - return TrinaryLogic::createYes(); + 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(); } @@ -201,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(); } @@ -217,94 +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, $offsetType), + $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType, + ), + new NonEmptyArrayType(), + ); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { return new self( - TypeCombinator::union($this->keyType, self::castToArrayKeyType($offsetType)), - $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType + $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; } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + public function fillKeysArray(Type $valueType): Type { - if ($this->isCallable()->no()) { - throw new \PHPStan\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 new ErrorType(); + return $this; } - public function toFloat(): Type + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + return $this; + } + + public function searchArray(Type $needleType): Type { - return new ErrorType(); + 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 new IntegerType(); + 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) { - /** @var int|string $offsetValue */ - $offsetValue = key([$offsetType->getValue() => 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 FloatType || $offsetType instanceof BooleanType) { - return new IntegerType(); - } + public function isCallable(): TrinaryLogic + { + return TrinaryLogic::createMaybe()->and($this->itemType->isString()); + } - if ($offsetType instanceof StringType) { - return $offsetType; - } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + { + if ($this->isCallable()->no()) { + throw new ShouldNotHappenException(); + } - if ($offsetType instanceof UnionType || $offsetType instanceof IntersectionType) { - return $traverse($offsetType); - } + return [new TrivialParametersAcceptor()]; + } + + 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 @@ -313,32 +489,9 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType instanceof ConstantArrayType && count($receivedType->getKeyTypes()) === 0) { - $keyType = $this->getKeyType(); - $typeMap = TemplateTypeMap::createEmpty(); - if ($keyType instanceof TemplateType) { - $typeMap = new TemplateTypeMap([ - $keyType->getName() => $keyType->getBound(), - ]); - } - - $itemType = $this->getItemType(); - if ($itemType instanceof TemplateType) { - $typeMap = $typeMap->union(new TemplateTypeMap([ - $itemType->getName() => $itemType->getBound(), - ])); - } - - return $typeMap; - } - - if ( - $receivedType instanceof ArrayType - && !$this->getKeyType()->isSuperTypeOf($receivedType->getKeyType())->no() - && !$this->getItemType()->isSuperTypeOf($receivedType->getItemType())->no() - ) { - $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getKeyType()); - $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getItemType()); + if ($receivedType->isArray()->yes()) { + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType()); return $keyTypeMap->union($itemTypeMap); } @@ -348,24 +501,11 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $keyVariance = $positionVariance; - $itemVariance = $positionVariance; - - if (!$positionVariance->contravariant()) { - $keyType = $this->getKeyType(); - if ($keyType instanceof TemplateType) { - $keyVariance = $keyType->getVariance(); - } - - $itemType = $this->getItemType(); - if ($itemType instanceof TemplateType) { - $itemVariance = $itemType->getVariance(); - } - } + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); return array_merge( - $this->getKeyType()->getReferencedTemplateTypes($keyVariance), - $this->getItemType()->getReferencedTemplateTypes($itemVariance) + $this->getIterableKeyType()->getReferencedTemplateTypes($variance), + $this->getItemType()->getReferencedTemplateTypes($variance), ); } @@ -375,22 +515,75 @@ public function traverse(callable $cb): Type $itemType = $cb($this->itemType); if ($keyType !== $this->keyType || $itemType !== $this->itemType) { + if ($keyType instanceof NeverType && $itemType instanceof NeverType) { + return new ConstantArrayType([], []); + } + return new self($keyType, $itemType); } return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function toPhpDocNode(): TypeNode { - return new self( - $properties['keyType'], - $properties['itemType'] + $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 new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + $this->keyType->toPhpDocNode(), + $this->itemType->toPhpDocNode(), + ], ); } + 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) { + if ($keyType instanceof NeverType && $itemType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new self($keyType, $itemType); + } + + return $this; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) { + return TypeCombinator::intersect($this, new NonEmptyArrayType()); + } + + if ($typeToRemove instanceof NonEmptyArrayType) { + return new ConstantArrayType([], []); + } + + return null; + } + + public function getFiniteTypes(): array + { + return []; + } + } diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index 65301394ed..6a2d28dc1c 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -3,10 +3,32 @@ namespace PHPStan\Type; use PHPStan\TrinaryLogic; +use PHPStan\Type\Generic\TemplateTypeMap; +use function count; +/** @api */ class BenevolentUnionType extends UnionType { + /** + * @api + * @param Type[] $types + */ + public function __construct(array $types, bool $normalized = false) + { + 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 { return '(' . parent::describe($level) . ')'; @@ -28,18 +50,132 @@ protected function unionTypes(callable $getType): Type return new ErrorType(); } - return TypeCombinator::union(...$resultTypes); + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$resultTypes)); + } + + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array + { + $values = []; + foreach ($this->getTypes() as $type) { + $innerValues = $getValues($type); + if ($innerValues === [] && $criteria($type)) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + + public function getOffsetValueType(Type $offsetType): Type + { + $types = []; + foreach ($this->getTypes() as $innerType) { + $valueType = $innerType->getOffsetValueType($offsetType); + if ($valueType instanceof ErrorType) { + continue; + } + + $types[] = $valueType; + } + + if (count($types) === 0) { + return new ErrorType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + + protected function unionResults(callable $getResult): TrinaryLogic + { + return TrinaryLogic::createNo()->lazyOr($this->getTypes(), $getResult); } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { + $result = AcceptsResult::createNo(); foreach ($this->getTypes() as $innerType) { - if ($acceptingType->accepts($innerType, $strictTypes)->yes()) { - return TrinaryLogic::createYes(); + $result = $result->or($acceptingType->accepts($innerType, $strictTypes)); + } + + return $result; + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + $types = TemplateTypeMap::createEmpty(); + + foreach ($this->getTypes() as $type) { + $types = $types->benevolentUnion($type->inferTemplateTypes($receivedType)); + } + + return $types; + } + + public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap + { + $types = TemplateTypeMap::createEmpty(); + + foreach ($this->getTypes() as $type) { + $types = $types->benevolentUnion($templateType->inferTemplateTypes($type)); + } + + return $types; + } + + public function traverse(callable $cb): Type + { + $types = []; + $changed = false; + + foreach ($this->getTypes() as $type) { + $newType = $cb($type); + if ($type !== $newType) { + $changed = true; } + $types[] = $newType; + } + + if ($changed) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof UnionType) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - return TrinaryLogic::createNo(); + 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 4db44aa216..a703decac4 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -2,26 +2,59 @@ 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; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +/** @api */ class BooleanType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; + use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + 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 { @@ -33,11 +66,16 @@ public function toNumber(): Type return $this->toInteger(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return TypeCombinator::union( new ConstantStringType(''), - new ConstantStringType('1') + new ConstantStringType('1'), ); } @@ -45,7 +83,7 @@ public function toInteger(): Type { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } @@ -53,7 +91,7 @@ public function toFloat(): Type { return TypeCombinator::union( new ConstantFloatType(0.0), - new ConstantFloatType(1.0) + new ConstantFloatType(1.0), ); } @@ -62,37 +100,98 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + [], + TrinaryLogic::createYes(), ); } - public function isOffsetAccessible(): TrinaryLogic + public function toArrayKey(): Type { - return TrinaryLogic::createNo(); + 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 hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type + 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) { + return new ConstantBooleanType(!$typeToRemove->getValue()); + } + + return null; + } + + public function getFiniteTypes(): array + { + return [ + new ConstantBooleanType(true), + new ConstantBooleanType(false), + ]; + } + + public function exponentiate(Type $exponent): Type { - return new ErrorType(); + return ExponentiateHelper::exponentiate($this, $exponent); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function toPhpDocNode(): TypeNode { - return new ErrorType(); + return new IdentifierTypeNode('bool'); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function toTrinaryLogic(): TrinaryLogic { - return new self(); + 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 2bc3416be7..568c847711 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -2,78 +2,155 @@ 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; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use function array_map; +use function array_merge; +use function count; -class CallableType implements CompoundType, ParametersAcceptor +/** @api */ +class CallableType implements CompoundType, CallableParametersAcceptor { + use MaybeArrayTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; use TruthyBooleanTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - /** @var array */ + /** @var list */ private array $parameters; private Type $returnType; - private bool $variadic; - private bool $isCommonCallable; + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + + private TrinaryLogic $isPure; + /** - * @param array $parameters - * @param Type $returnType - * @param bool $variadic + * @api + * @param list|null $parameters + * @param array $templateTags */ public function __construct( ?array $parameters = null, ?Type $returnType = null, - bool $variadic = true + 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->variadic = $variadic; $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 = []; + foreach ($this->parameters as $parameter) { + $classes = array_merge($classes, $parameter->getType()->getReferencedClasses()); + } + + return array_merge($classes, $this->returnType->getReferencedClasses()); + } + + public function getObjectClassNames(): array { return []; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + 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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + 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; } @@ -82,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; @@ -93,50 +189,61 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina } if ($variantsResult === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } 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 function (): string { - return 'callable'; - }, - function () use ($level): string { - return sprintf( - 'callable(%s): %s', - implode(', ', array_map( - static function (NativeParameterReflection $param) use ($level): string { - return sprintf('%s%s', $param->isVariadic() ? '...' : '', $param->getType()->describe($level)); - }, - $this->getParameters() - )), - $this->returnType->describe($level) + static fn (): string => 'callable', + 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()); + }, ); } @@ -145,20 +252,59 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\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(); @@ -179,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 { @@ -213,7 +384,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType->isCallable()->no()) { + if (! $receivedType->isCallable()->yes()) { return TemplateTypeMap::createEmpty(); } @@ -222,22 +393,31 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeMap = TemplateTypeMap::createEmpty(); foreach ($parametersAcceptors as $parametersAcceptor) { - $typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($receivedType, $parametersAcceptor)); + $typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($parametersAcceptor)); } return $typeMap; } - private function inferTemplateTypesOnParametersAcceptor(Type $receivedType, ParametersAcceptor $parametersAcceptor): 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) { - $argType = isset($args[$i]) ? $args[$i]->getType() : new NeverType(); $paramType = $param->getType(); - $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)); + if (isset($args[$i])) { + $argType = $args[$i]->getType(); + } elseif ($paramType instanceof TemplateType) { + $argType = TemplateTypeHelper::resolveToBounds($paramType); + } else { + $argType = new NeverType(); + } + + $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)->convertToLowerBoundTypes()); } return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType)); @@ -246,7 +426,7 @@ private function inferTemplateTypesOnParametersAcceptor(Type $receivedType, Para public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { $references = $this->getReturnType()->getReferencedTemplateTypes( - $positionVariance->compose(TemplateTypeVariance::createCovariant()) + $positionVariance->compose(TemplateTypeVariance::createCovariant()), ); $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant()); @@ -266,7 +446,7 @@ public function traverse(callable $cb): Type return $this; } - $parameters = array_map(static function (NativeParameterReflection $param) use ($cb): NativeParameterReflection { + $parameters = array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); return new NativeParameterReflection( $param->getName(), @@ -274,32 +454,242 @@ public function traverse(callable $cb): Type $cb($param->getType()), $param->passedByReference(), $param->isVariadic(), - $defaultValue !== null ? $cb($defaultValue) : null + $defaultValue !== null ? $cb($defaultValue) : null, ); }, $this->getParameters()); return new self( $parameters, $cb($this->getReturnType()), - $this->isVariadic() + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, + ); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->isCommonCallable) { + return $this; + } + + if (!$right->isCallable()->yes()) { + return $this; + } + + $rightAcceptors = $right->getCallableParametersAcceptors(new OutOfClassScope()); + if (count($rightAcceptors) !== 1) { + return $this; + } + + $rightParameters = $rightAcceptors[0]->getParameters(); + if (count($this->getParameters()) !== count($rightParameters)) { + return $this; + } + + $parameters = []; + foreach ($this->getParameters() as $i => $leftParam) { + $rightParam = $rightParameters[$i]; + $leftDefaultValue = $leftParam->getDefaultValue(); + $rightDefaultValue = $rightParam->getDefaultValue(); + $defaultValue = $leftDefaultValue; + if ($leftDefaultValue !== null && $rightDefaultValue !== null) { + $defaultValue = $cb($leftDefaultValue, $rightDefaultValue); + } + $parameters[] = new NativeParameterReflection( + $leftParam->getName(), + $leftParam->isOptional(), + $cb($leftParam->getType(), $rightParam->getType()), + $leftParam->passedByReference(), + $leftParam->isVariadic(), + $defaultValue, + ); + } + + return new self( + $parameters, + $cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()), + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, ); } - public function isArray(): TrinaryLogic + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isNumericString(): TrinaryLogic { - return new self( - (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], - (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], - $properties['variadic'] + return TrinaryLogic::createNo(); + } + + 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; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable'); + } + + $parameters = []; + foreach ($this->parameters as $parameter) { + $parameters[] = new CallableTypeParameterNode( + $parameter->getType()->toPhpDocNode(), + !$parameter->passedByReference()->no(), + $parameter->isVariadic(), + $parameter->getName() === '' ? '' : '$' . $parameter->getName(), + $parameter->isOptional(), + ); + } + + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + + return new CallableTypeNode( + new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, ); } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 85698a22b3..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, - bool $treatMixedAsAny - ): TrinaryLogic + CallableParametersAcceptor $ours, + CallableParametersAcceptor $theirs, + bool $treatMixedAsAny, + ): 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 new file mode 100644 index 0000000000..d0502cb9a1 --- /dev/null +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -0,0 +1,10 @@ +isAcceptedBy($this, $strictTypes); } - if ($type instanceof ConstantStringType) { - $broker = Broker::getInstance(); - return TrinaryLogic::createFromBoolean($broker->hasClass($type->getValue())); - } + return new AcceptsResult($type->isClassString(), []); + } - if ($type instanceof self) { - return TrinaryLogic::createYes(); + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); } - if ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); - } + return new IsSuperTypeOfResult($type->isClassString(), []); + } - return TrinaryLogic::createNo(); + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isNumericString(): TrinaryLogic { - if ($type instanceof ConstantStringType) { - $broker = Broker::getInstance(); - return TrinaryLogic::createFromBoolean($broker->hasClass($type->getValue())); - } + return TrinaryLogic::createMaybe(); + } - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); - } + 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::createYes(); + } - return TrinaryLogic::createNo(); + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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 79059cbe49..9a12b00fbb 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -2,51 +2,153 @@ namespace PHPStan\Type; +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\ConstantReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\ClosureCallMethodReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\Php\ClosureCallUnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\Traits\NonGenericTypeTrait; - -class ClosureType implements TypeWithClassName, ParametersAcceptor +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Traits\NonArrayTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +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 count; + +/** @api */ +class ClosureType implements TypeWithClassName, CallableParametersAcceptor { - use NonGenericTypeTrait; - - private ObjectType $objectType; + use NonArrayTypeTrait; + use NonIterableTypeTrait; + use UndecidedComparisonTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - /** @var array */ + /** @var list */ private array $parameters; private Type $returnType; - private bool $variadic; + private bool $isCommonCallable; + + private ObjectType $objectType; + + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + + private TemplateTypeVarianceMap $callSiteVarianceMap; + + /** @var SimpleImpurePoint[] */ + private array $impurePoints; + + private TrinaryLogic $acceptsNamedArguments; /** - * @param array $parameters - * @param Type $returnType - * @param bool $variadic + * @api + * @param list|null $parameters + * @param array $templateTags + * @param SimpleThrowPoint[] $throwPoints + * @param ?SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables */ public function __construct( - array $parameters, - Type $returnType, - 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, ) { - $this->objectType = new ObjectType(\Closure::class); - $this->parameters = $parameters; - $this->returnType = $returnType; - $this->variadic = $variadic; + 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 @@ -54,14 +156,16 @@ public function getClassName(): string return $this->objectType->getClassName(); } - public function getAncestorWithClassName(string $className): ?ObjectType + public function getClassReflection(): ?ClassReflection + { + return $this->objectType->getClassReflection(); + } + + public function getAncestorWithClassName(string $className): ?TypeWithClassName { return $this->objectType->getAncestorWithClassName($className); } - /** - * @return string[] - */ public function getReferencedClasses(): array { $classes = $this->objectType->getReferencedClasses(); @@ -72,39 +176,55 @@ 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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof ClosureType) { 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, - $treatMixedAsAny + $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); @@ -116,20 +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 sprintf( - 'Closure(%s): %s', - implode(', ', array_map(static function (ParameterReflection $parameter) use ($level): string { - return sprintf('%s%s', $parameter->isVariadic() ? '...' : '', $parameter->getType()->describe($level)); - }, $this->parameters)), - $this->returnType->describe($level) + return $level->handle( + static fn (): string => 'Closure', + 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(); @@ -140,11 +305,16 @@ 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); } + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->objectType->getUnresolvedPropertyPrototype($propertyName, $scope); + } + public function canCallMethods(): TrinaryLogic { return $this->objectType->canCallMethods(); @@ -155,16 +325,21 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { if ($methodName === 'call') { - return new ClosureCallMethodReflection( - $this->objectType->getMethod($methodName, $scope), - $this + return new ClosureCallUnresolvedMethodPrototypeReflection( + $this->objectType->getUnresolvedMethodPrototype($methodName, $scope), + $this, ); } - return $this->objectType->getMethod($methodName, $scope); + return $this->objectType->getUnresolvedMethodPrototype($methodName, $scope); } public function canAccessConstants(): TrinaryLogic @@ -177,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(); @@ -192,48 +372,49 @@ 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 isOffsetAccessible(): TrinaryLogic + public function isCommonCallable(): bool { - return TrinaryLogic::createNo(); + return $this->isCommonCallable; } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - return TrinaryLogic::createNo(); + return [$this]; } - public function getOffsetValueType(Type $offsetType): Type + public function getThrowPoints(): array { - return new ErrorType(); + return $this->throwPoints; } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function getImpurePoints(): array { - return new ErrorType(); + return $this->impurePoints; } - public function isCallable(): TrinaryLogic + public function getInvalidateExpressions(): array { - return TrinaryLogic::createYes(); + return $this->invalidateExpressions; } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + public function getUsedVariables(): array { - return [$this]; + return $this->usedVariables; + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->acceptsNamedArguments; } public function isCloneable(): TrinaryLogic @@ -251,6 +432,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -271,22 +457,39 @@ 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 TemplateTypeMap::createEmpty(); + return $this->templateTypeMap; } public function getResolvedTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->resolvedTemplateTypeMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; } /** - * @return array + * @return list */ public function getParameters(): array { @@ -309,7 +512,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType->isCallable()->no()) { + if ($receivedType->isCallable()->no() || ! $receivedType instanceof self) { return TemplateTypeMap::createEmpty(); } @@ -318,31 +521,61 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeMap = TemplateTypeMap::createEmpty(); foreach ($parametersAcceptors as $parametersAcceptor) { - $typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($receivedType, $parametersAcceptor)); + $typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($parametersAcceptor)); } return $typeMap; } - private function inferTemplateTypesOnParametersAcceptor(Type $receivedType, ParametersAcceptor $parametersAcceptor): 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) { - $argType = isset($args[$i]) ? $args[$i]->getType() : new NeverType(); $paramType = $param->getType(); - $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)); + if (isset($args[$i])) { + $argType = $args[$i]->getType(); + } elseif ($paramType instanceof TemplateType) { + $argType = TemplateTypeHelper::resolveToBounds($paramType); + } else { + $argType = new NeverType(); + } + + $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)->convertToLowerBoundTypes()); } 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 (NativeParameterReflection $param) use ($cb): NativeParameterReflection { + array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); return new NativeParameterReflection( $param->getName(), @@ -350,29 +583,229 @@ public function traverse(callable $cb): Type $cb($param->getType()), $param->passedByReference(), $param->isVariadic(), - $defaultValue !== null ? $cb($defaultValue) : null + $defaultValue !== null ? $cb($defaultValue) : null, ); }, $this->getParameters()), $cb($this->getReturnType()), - $this->isVariadic() + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $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(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isConstantValue(): TrinaryLogic { - return new self( - $properties['parameters'], - $properties['returnType'], - $properties['variadic'] + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + 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 $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/CommentHelper.php b/src/Type/CommentHelper.php deleted file mode 100644 index 7681e75aa7..0000000000 --- a/src/Type/CommentHelper.php +++ /dev/null @@ -1,20 +0,0 @@ -getDocComment(); - if ($phpDoc !== null) { - return $phpDoc->getText(); - } - - return null; - } - -} diff --git a/src/Type/CompoundType.php b/src/Type/CompoundType.php index 7513e3d2bb..f66e10c091 100644 --- a/src/Type/CompoundType.php +++ b/src/Type/CompoundType.php @@ -2,13 +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, PhpVersion $phpVersion): TrinaryLogic; + + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; } diff --git a/src/Type/CompoundTypeHelper.php b/src/Type/CompoundTypeHelper.php deleted file mode 100644 index a86e36eb5e..0000000000 --- a/src/Type/CompoundTypeHelper.php +++ /dev/null @@ -1,15 +0,0 @@ -isAcceptedBy($otherType, $strictTypes); - } - -} diff --git a/src/Type/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 7f011d740c..01476d4d01 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2,85 +2,194 @@ namespace PHPStan\Type\Constant; -use PHPStan\Broker\Broker; +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\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\Generic\TemplateMixedType; +use PHPStan\Type\GeneralizePrecision; 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\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\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function array_keys; +use function array_map; +use function array_merge; +use function array_pop; +use function array_push; +use function array_slice; use function array_unique; - -class ConstantArrayType extends ArrayType implements ConstantType +use function array_values; +use function assert; +use function count; +use function implode; +use function in_array; +use function is_string; +use function min; +use function pow; +use function range; +use function sort; +use function sprintf; +use function str_contains; + +/** + * @api + */ +class ConstantArrayType implements Type { - private const DESCRIBE_LIMIT = 8; - - /** @var array */ - private array $keyTypes; - - /** @var array */ - private array $valueTypes; + use ArrayTypeTrait { + chunkArray as traitChunkArray; + } + use NonObjectTypeTrait; + use UndecidedComparisonTypeTrait; - private int $nextAutoIndex; + private const DESCRIBE_LIMIT = 8; + private const CHUNK_FINITE_TYPES_LIMIT = 5; - /** @var int[] */ - private array $optionalKeys; + 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 int $nextAutoIndex + * @param non-empty-list $nextAutoIndexes * @param int[] $optionalKeys */ public function __construct( - array $keyTypes, - array $valueTypes, - int $nextAutoIndex = 0, - array $optionalKeys = [] + private array $keyTypes, + private array $valueTypes, + 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(), - count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType() - ); + $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(); + } - $this->keyTypes = $keyTypes; - $this->valueTypes = $valueTypes; - $this->nextAutoIndex = $nextAutoIndex; - $this->optionalKeys = $optionalKeys; + 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; } /** @@ -120,14 +229,20 @@ 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) { - throw new \PHPStan\ShouldNotHappenException(); + if (!$array instanceof self) { + throw new ShouldNotHappenException(); } $arrays[] = $array; @@ -162,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 */ @@ -192,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; @@ -213,61 +323,123 @@ 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) { - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createYes(); + return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []); } $results = []; foreach ($this->keyTypes as $i => $keyType) { $hasOffset = $type->hasOffsetValueType($keyType); if ($hasOffset->no()) { - return TrinaryLogic::createNo(); + if (!$this->isOptionalKey($i)) { + return IsSuperTypeOfResult::createNo(); + } + + $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 @@ -299,101 +471,148 @@ 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); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - $typeAndMethodName = $this->findTypeAndMethodName(); - if ($typeAndMethodName === null) { - throw new \PHPStan\ShouldNotHappenException(); + $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; } - private 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) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($classOrObject->getValue())) { - return ConstantArrayTypeAndMethod::createUnknown(); - } - $type = new ObjectType($broker->getClass($classOrObject->getValue())->getName()); - } elseif ((new \PHPStan\Type\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); + $offsetArrayKeyType = $offsetType->toArrayKey(); + + return $this->recursiveHasOffsetValueType($offsetArrayKeyType); + } + + private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic + { if ($offsetType instanceof UnionType) { $results = []; foreach ($offsetType->getTypes() as $innerType) { - $results[] = $this->hasOffsetValueType($innerType); + $results[] = $this->recursiveHasOffsetValueType($innerType); } return TrinaryLogic::extremeIdentity(...$results); } + if ($offsetType instanceof IntegerRangeType) { + $finiteTypes = $offsetType->getFiniteTypes(); + if ($finiteTypes !== []) { + $results = []; + foreach ($finiteTypes as $innerType) { + $results[] = $this->recursiveHasOffsetValueType($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(); } @@ -417,16 +636,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) { @@ -436,10 +675,14 @@ public function getOffsetValueType(Type $offsetType): Type return $type; } + if ($maybeAll) { + return $this->getIterableValueType(); + } + return new ErrorType(); // undefined offset } - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = false): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); $builder->setOffsetValueType($offsetType, $valueType); @@ -447,222 +690,641 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $builder->getArray(); } - public function unsetOffset(Type $offsetType): Type + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - $offsetType = ArrayType::castToArrayKeyType($offsetType); - 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++; - } - - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndex, $newOptionalKeys); - } + $offsetType = $offsetType->toArrayKey(); + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + foreach ($this->keyTypes as $keyType) { + if ($offsetType->isSuperTypeOf($keyType)->no()) { + continue; } + + $builder->setOffsetValueType($keyType, $valueType); } - return $this->generalize(); + return $builder->getArray(); } - public function isIterableAtLeastOnce(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createFromBoolean(count($this->keyTypes) > 0); - } + $offsetType = $offsetType->toArrayKey(); + if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { + foreach ($this->keyTypes as $i => $keyType) { + 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()); + } - public function removeLast(): self - { - if (count($this->keyTypes) === 0) { return $this; } - $i = count($this->keyTypes) - 1; + $constantScalars = $offsetType->getConstantScalarTypes(); + if (count($constantScalars) > 0) { + $optionalKeys = $this->optionalKeys; - $keyTypes = $this->keyTypes; - $valueTypes = $this->valueTypes; - $optionalKeys = $this->optionalKeys; - unset($optionalKeys[$i]); + foreach ($constantScalars as $constantScalar) { + $constantScalar = $constantScalar->toArrayKey(); + if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) { + continue; + } - $removedKeyType = array_pop($keyTypes); - array_pop($valueTypes); - $nextAutoindex = $removedKeyType instanceof ConstantIntegerType - ? $removedKeyType->getValue() - : $this->nextAutoIndex; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $constantScalar->getValue()) { + continue; + } - return new self( - $keyTypes, - $valueTypes, - $nextAutoindex, - array_values($optionalKeys) - ); - } + if (in_array($i, $optionalKeys, true)) { + continue 2; + } - public function removeFirst(): ArrayType - { - $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($this->keyTypes as $i => $keyType) { - if ($i === 0) { - continue; + $optionalKeys[] = $i; + } } - $valueType = $this->valueTypes[$i]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; - } + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); + } - $builder->setOffsetValueType($keyType, $valueType); + $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 $builder->getArray(); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList); } - public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self + public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type { - if (count($this->keyTypes) === 0) { - return $this; - } + $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); + } - $keyTypes = array_slice($this->keyTypes, $offset, $limit); - $valueTypes = array_slice($this->valueTypes, $offset, $limit); + $length = $finiteType->getValue(); - 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); - } + $builder = ConstantArrayTypeBuilder::createEmpty(); - return $keyType; - }, $keyTypes); - } + $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()); + } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = 0; - foreach ($keyTypes as $keyType) { - if (!$keyType instanceof ConstantIntegerType) { - continue; + $results[] = $builder->getArray(); } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1); + return TypeCombinator::union(...$results); } - return new self( - $keyTypes, - $valueTypes, - (int) $nextAutoIndex, - [] - ); + return $this->traitChunkArray($lengthType, $preserveKeys); } - public function toBoolean(): BooleanType + public function fillKeysArray(Type $valueType): Type { - return new ConstantBooleanType(count($this->keyTypes) > 0); - } + $builder = ConstantArrayTypeBuilder::createEmpty(); - public function generalize(): Type - { - if (count($this->keyTypes) === 0) { - return $this; + foreach ($this->valueTypes as $i => $keyType) { + if ($keyType->isInteger()->no()) { + $stringKeyType = $keyType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i)); + } else { + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i)); + } } - return new ArrayType( - TypeUtils::generalizeType($this->getKeyType()), - $this->getItemType() - ); + return $builder->getArray(); } - /** - * @return self - */ - public function generalizeValues(): ArrayType + public function flipArray(): Type { - $valueTypes = []; - foreach ($this->valueTypes as $valueType) { - $valueTypes[] = TypeUtils::generalizeType($valueType); + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->keyTypes as $i => $keyType) { + $valueType = $this->valueTypes[$i]; + $builder->setOffsetValueType( + $valueType->toArrayKey(), + $keyType, + $this->isOptionalKey($i), + ); } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return $builder->getArray(); } - /** - * @return self - */ - public function getKeysArray(): ArrayType + public function intersectKeyArray(Type $otherArraysType): Type { - $keyTypes = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; + $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($this->keyTypes as $i => $keyType) { - $keyTypes[] = new ConstantIntegerType($i); - $valueTypes[] = $keyType; - $autoIndex++; - - if (!$this->isOptionalKey($i)) { + $valueType = $this->valueTypes[$i]; + $has = $otherArraysType->hasOffsetValueType($keyType); + if ($has->no()) { continue; } - - $optionalKeys[] = $i; + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes()); } - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return $builder->getArray(); } - /** - * @return self - */ - public function getValuesArray(): ArrayType + public function popArray(): Type { - $keyTypes = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; - - foreach ($this->valueTypes as $i => $valueType) { - $keyTypes[] = new ConstantIntegerType($i); - $valueTypes[] = $valueType; - $autoIndex++; + return $this->removeLastElements(1); + } - if (!$this->isOptionalKey($i)) { - continue; - } + public function reverseArray(TrinaryLogic $preserveKeys): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); - $optionalKeys[] = $i; + 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 new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return $builder->getArray(); } - public function count(): Type + public function searchArray(Type $needleType): Type { - $optionalKeysCount = count($this->optionalKeys); - $totalKeysCount = count($this->getKeyTypes()); - if ($optionalKeysCount === $totalKeysCount) { - return new ConstantIntegerType($totalKeysCount); + $matches = []; + $hasIdenticalValue = false; + + foreach ($this->valueTypes as $index => $valueType) { + $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); + if ($isNeedleSuperType->no()) { + continue; + } + + if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType + && $needleType->getValue() === $valueType->getValue() + && !$this->isOptionalKey($index) + ) { + $hasIdenticalValue = true; + } + + $matches[] = $this->keyTypes[$index]; + } + + if (count($matches) > 0) { + if ($hasIdenticalValue) { + return TypeCombinator::union(...$matches); + } + + return TypeCombinator::union(new ConstantBooleanType(false), ...$matches); + } + + return new ConstantBooleanType(false); + } + + public function shiftArray(): Type + { + return $this->removeFirstElements(1); + } + + public function shuffleArray(): Type + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this->getValuesArray()); + $builder->degradeToGeneralArray(); + + return $builder->getArray(); + } + + public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type + { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return $this; + } + + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null; + + if ($lengthType instanceof ConstantIntegerType) { + $length = $lengthType->getValue(); + } elseif ($lengthType->isNull()->yes()) { + $length = $keyTypesCount; + } else { + $length = null; + } + + if ($offset === null || $length === null) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->degradeToGeneralArray(); + + return $builder->getArray() + ->sliceArray($offsetType, $lengthType, $preserveKeys); + } + + if ($keyTypesCount + $offset <= 0) { + // A negative offset cannot reach left outside the array twice + $offset = 0; + } + + 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 $builder->getArray(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + $keysCount = count($this->keyTypes); + if ($keysCount === 0) { + return TrinaryLogic::createNo(); + } + + $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)) { + break; + } + } + + return TypeCombinator::union(...$keyTypes); + } + + public function getLastIterableKeyType(): Type + { + $keyTypes = []; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $keyTypes[] = $this->keyTypes[$i]; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$keyTypes); + } + + public function getFirstIterableValueType(): Type + { + $valueTypes = []; + foreach ($this->valueTypes as $i => $valueType) { + $valueTypes[] = $valueType; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + + public function getLastIterableValueType(): Type + { + $valueTypes = []; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $valueTypes[] = $this->valueTypes[$i]; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isList(): TrinaryLogic + { + return $this->isList; + } + + /** @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 $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 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); + + $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(); + } + + 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 { $describeValue = function (bool $truncate) use ($level): string { @@ -680,8 +1342,18 @@ public function describe(VerbosityLevel $level): string $exportValuesOnly = false; } - $items[] = sprintf('%s%s => %s', $isOptional ? '?' : '', var_export($keyType->getValue(), true), $valueType->describe($level)); - $values[] = $valueType->describe($level); + $keyDescription = $keyType->getValue(); + if (is_string($keyDescription)) { + if (str_contains($keyDescription, '"')) { + $keyDescription = sprintf('\'%s\'', $keyDescription); + } elseif (str_contains($keyDescription, '\'')) { + $keyDescription = sprintf('"%s"', $keyDescription); + } + } + + $valueTypeDescription = $valueType->describe($level); + $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueTypeDescription); + $values[] = $valueTypeDescription; } $append = ''; @@ -692,21 +1364,15 @@ public function describe(VerbosityLevel $level): string } return sprintf( - 'array(%s%s)', + 'array{%s%s}', implode(', ', $exportValuesOnly ? $values : $items), - $append + $append, ); }; return $level->handle( - function () use ($level): string { - return parent::describe($level); - }, - static function () use ($describeValue): string { - return $describeValue(true); - }, - static function () use ($describeValue): string { - return $describeValue(false); - } + fn (): string => $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), ); } @@ -716,10 +1382,13 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType instanceof self && !$this->isSuperTypeOf($receivedType)->no()) { + if ($receivedType instanceof self) { $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->keyTypes as $i => $keyType) { $valueType = $this->valueTypes[$i]; + if ($receivedType->hasOffsetValueType($keyType)->no()) { + continue; + } $receivedValueType = $receivedType->getOffsetValueType($keyType); $typeMap = $typeMap->union($valueType->inferTemplateTypes($receivedValueType)); } @@ -727,8 +1396,11 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $typeMap; } - if ($receivedType instanceof ArrayType) { - 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(); @@ -736,7 +1408,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $variance = $positionVariance->compose(TemplateTypeVariance::createInvariant()); + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); $references = []; foreach ($this->keyTypes as $type) { @@ -754,27 +1426,60 @@ 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 { - $keyTypes = []; $valueTypes = []; $stillOriginal = true; - foreach ($this->keyTypes as $keyType) { - $transformedKeyType = $cb($keyType); - if ($transformedKeyType !== $keyType) { + foreach ($this->valueTypes as $valueType) { + $transformedValueType = $cb($valueType); + if ($transformedValueType !== $valueType) { $stillOriginal = false; } - if (!$transformedKeyType instanceof ConstantIntegerType && !$transformedKeyType instanceof ConstantStringType) { - throw new \PHPStan\ShouldNotHappenException(); - } + $valueTypes[] = $transformedValueType; + } - $keyTypes[] = $transformedKeyType; + if ($stillOriginal) { + return $this; } - foreach ($this->valueTypes as $valueType) { - $transformedValueType = $cb($valueType); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right->isArray()->yes()) { + return $this; + } + + $valueTypes = []; + + $stillOriginal = true; + foreach ($this->valueTypes as $i => $valueType) { + $keyType = $this->keyTypes[$i]; + $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType)); if ($transformedValueType !== $valueType) { $stillOriginal = false; } @@ -786,37 +1491,64 @@ public function traverse(callable $cb): Type return $this; } - return new self($keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } public function isKeysSupersetOf(self $otherArray): bool { - if (count($this->keyTypes) === 0) { - return count($otherArray->keyTypes) === 0; - } + $keyTypesCount = count($this->keyTypes); + $otherKeyTypesCount = count($otherArray->keyTypes); - if (count($otherArray->keyTypes) === 0) { + 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; - unset($otherKeys[$j]); - continue 2; + 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; + } + + if ($failOnDifferentValueType) { + return false; + } + $failOnDifferentValueType = true; + } + + $requiredKeyCount = 0; + foreach (array_keys($keyTypes) as $i) { + if ($this->isOptionalKey($i)) { + continue; + } + + $requiredKeyCount++; + if ($requiredKeyCount > 1) { + return false; } } - return count($otherKeys) === 0; + 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) { @@ -834,16 +1566,27 @@ 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)); } /** * @param ConstantIntegerType|ConstantStringType $otherKeyType - * @return int|null */ 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; } @@ -854,7 +1597,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)) { @@ -864,7 +1607,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); } } @@ -874,13 +1617,93 @@ public function makeOffsetRequired(Type $offsetType): self return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - 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 + { + $result = Strings::match($value, '~^(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++$~si'); + + return $result !== null; + } + + public function getFiniteTypes(): array { - return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []); + $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 548254e1e0..07f4156550 100644 --- a/src/Type/Constant/ConstantArrayTypeAndMethod.php +++ b/src/Type/Constant/ConstantArrayTypeAndMethod.php @@ -2,37 +2,32 @@ namespace PHPStan\Type\Constant; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class ConstantArrayTypeAndMethod +/** + * @api + */ +final class ConstantArrayTypeAndMethod { - private ?\PHPStan\Type\Type $type; - - private ?string $method; - - private TrinaryLogic $certainty; - private function __construct( - ?Type $type, - ?string $method, - TrinaryLogic $certainty + private ?Type $type, + private ?string $method, + private TrinaryLogic $certainty, ) { - $this->type = $type; - $this->method = $method; - $this->certainty = $certainty; } public static function createConcrete( Type $type, string $method, - TrinaryLogic $certainty + TrinaryLogic $certainty, ): self { if ($certainty->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($type, $method, $certainty); } @@ -50,7 +45,7 @@ public function isUnknown(): bool public function getType(): Type { if ($this->type === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->type; @@ -59,7 +54,7 @@ public function getType(): Type public function getMethod(): string { if ($this->method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->method; diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 84fce75847..a639bf6c0e 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -2,123 +2,326 @@ 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; -class ConstantArrayTypeBuilder +/** + * @api + */ +final class ConstantArrayTypeBuilder { - /** @var array */ - private array $keyTypes; - - /** @var array */ - private array $valueTypes; - - /** @var array */ - private array $optionalKeys; - - private int $nextAutoIndex; + public const ARRAY_COUNT_LIMIT = 256; private bool $degradeToGeneralArray = false; + private bool $oversized = false; + /** - * @param array $keyTypes + * @param array $keyTypes * @param array $valueTypes + * @param non-empty-list $nextAutoIndexes * @param array $optionalKeys - * @param int $nextAutoIndex */ private function __construct( - array $keyTypes, - array $valueTypes, - int $nextAutoIndex, - array $optionalKeys + private array $keyTypes, + private array $valueTypes, + private array $nextAutoIndexes, + private array $optionalKeys, + private TrinaryLogic $isList, ) { - $this->keyTypes = $keyTypes; - $this->valueTypes = $valueTypes; - $this->nextAutoIndex = $nextAutoIndex; - $this->optionalKeys = $optionalKeys; } public static function createEmpty(): self { - return new self([], [], 0, []); + return new self([], [], [0], [], TrinaryLogic::createYes()); } public static function createFromConstantArray(ConstantArrayType $startArrayType): self { - return new self( + $builder = new self( $startArrayType->getKeyTypes(), $startArrayType->getValueTypes(), - $startArrayType->getNextAutoIndex(), - $startArrayType->getOptionalKeys() + $startArrayType->getNextAutoIndexes(), + $startArrayType->getOptionalKeys(), + $startArrayType->isList(), ); + + if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) { + $builder->degradeToGeneralArray(true); + } + + return $builder; } 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 - && ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) - ) { - /** @var ConstantIntegerType|ConstantStringType $keyType */ - foreach ($this->keyTypes as $i => $keyType) { - if ($keyType->getValue() === $offsetType->getValue()) { + 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()) { + continue; + } + + if ($optional) { + $valueType = TypeCombinator::union($valueType, $this->valueTypes[$i]); + } + $this->valueTypes[$i] = $valueType; - $this->optionalKeys = array_values(array_filter($this->optionalKeys, static function (int $index) use ($i): bool { - return $index !== $i; - })); + + if (!$optional) { + $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i)); + 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 ($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(); + } + + if ($optional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; + } + + if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { + $this->degradeToGeneralArray = true; + $this->oversized = true; + } + + return; } - $this->keyTypes[] = $offsetType; - $this->valueTypes[] = $valueType; + $scalarTypes = $offsetType->getConstantScalarTypes(); + if (count($scalarTypes) === 0) { + $integerRanges = TypeUtils::getIntegerRanges($offsetType); + if (count($integerRanges) > 0) { + foreach ($integerRanges as $integerRange) { + if ($integerRange->getMin() === null) { + break; + } + if ($integerRange->getMax() === null) { + break; + } + + $rangeLength = $integerRange->getMax() - $integerRange->getMin(); + if ($rangeLength >= self::ARRAY_COUNT_LIMIT) { + $scalarTypes = []; + break; + } - if ($optional) { - $this->optionalKeys[] = count($this->keyTypes) - 1; + foreach (range($integerRange->getMin(), $integerRange->getMax()) as $rangeValue) { + $scalarTypes[] = new ConstantIntegerType($rangeValue); + } + } + } } + if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) { + $match = true; + $valueTypes = $this->valueTypes; + foreach ($scalarTypes as $scalarType) { + $scalarOffsetType = $scalarType->toArrayKey(); + if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) { + throw new ShouldNotHappenException(); + } + $offsetMatch = false; + + /** @var ConstantIntegerType|ConstantStringType $keyType */ + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $scalarOffsetType->getValue()) { + continue; + } - /** @var int|float $newNextAutoIndex */ - $newNextAutoIndex = $offsetType instanceof ConstantIntegerType - ? max($this->nextAutoIndex, $offsetType->getValue() + 1) - : $this->nextAutoIndex; - if (!is_float($newNextAutoIndex)) { - $this->nextAutoIndex = $newNextAutoIndex; + $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType); + $offsetMatch = true; + } + + if ($offsetMatch) { + continue; + } + + $match = false; + } + + if ($match) { + $this->valueTypes = $valueTypes; + return; + } } - 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; $this->valueTypes[] = $valueType; + if ($optional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; + } $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(): ArrayType + public function getArray(): Type { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { + return new ConstantArrayType([], []); + } + 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); } - return new ArrayType( + $array = new ArrayType( TypeCombinator::union(...$this->keyTypes), - TypeCombinator::union(...$this->valueTypes) + TypeCombinator::union(...$this->valueTypes), ); + + if (count($this->optionalKeys) < $keyTypesCount) { + $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 104f2d8ae2..282b005c15 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -2,22 +2,33 @@ 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; - - private bool $value; + use ConstantScalarTypeTrait { + looseCompare as private scalarLooseCompare; + } - public function __construct(bool $value) + /** @api */ + public function __construct(private bool $value) { - $this->value = $value; + parent::__construct(); } public function getValue(): bool @@ -30,6 +41,38 @@ public function describe(VerbosityLevel $level): string return $this->value ? 'true' : 'false'; } + public function getSmallerType(PhpVersion $phpVersion): Type + { + if ($this->value) { + return StaticTypeFactory::falsey(); + } + return new NeverType(); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + if ($this->value) { + return new MixedType(); + } + return StaticTypeFactory::falsey(); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + if ($this->value) { + return new NeverType(); + } + return StaticTypeFactory::truthy(); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + if ($this->value) { + return StaticTypeFactory::truthy(); + } + return new MixedType(); + } + public function toBoolean(): BooleanType { return $this; @@ -40,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); @@ -55,13 +103,47 @@ public function toFloat(): Type return new ConstantFloatType((float) $this->value); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function toArrayKey(): Type { - return new self($properties['value']); + 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 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 8a3563ed25..0ac763af76 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -2,25 +2,35 @@ 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 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 { use ConstantScalarTypeTrait; use ConstantScalarToBooleanTrait; + use ConstantNumericComparisonTypeTrait; - private float $value; - - public function __construct(float $value) + /** @api */ + public function __construct(private float $value) { - $this->value = $value; + parent::__construct(); } public function getValue(): float @@ -28,46 +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 function (): string { - return '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 @@ -80,13 +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 Type + * @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 93c8694e70..6b482c62e6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -2,26 +2,35 @@ 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 */ class ConstantIntegerType extends IntegerType implements ConstantScalarType { use ConstantScalarTypeTrait; use ConstantScalarToBooleanTrait; + use ConstantNumericComparisonTypeTrait; - private int $value; - - public function __construct(int $value) + /** @api */ + public function __construct(private int $value) { - $this->value = $value; + parent::__construct(); } public function getValue(): int @@ -29,41 +38,38 @@ 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) { - if ($type->getMin() <= $this->value && $this->value <= $type->getMax()) { - return TrinaryLogic::createMaybe(); + $min = $type->getMin(); + $max = $type->getMax(); + if (($min === null || $min <= $this->value) && ($max === null || $this->value <= $max)) { + 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 { return $level->handle( - static function (): string { - return 'int'; - }, - function (): string { - return sprintf('%s', $this->value); - } + static fn (): string => 'int', + fn (): string => sprintf('%s', $this->value), ); } @@ -72,18 +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 Type + * @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 d6afa4021e..e4c34e609f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -2,26 +2,61 @@ namespace PHPStan\Type\Constant; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node\Name; -use PHPStan\Broker\Broker; +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\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; use PHPStan\Type\ErrorType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\ConstantScalarTypeTrait; 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 substr_count; + +/** @api */ class ConstantStringType extends StringType implements ConstantScalarType { @@ -30,14 +65,14 @@ class ConstantStringType extends StringType implements ConstantScalarType use ConstantScalarTypeTrait; use ConstantScalarToBooleanTrait; - private string $value; + private ?ObjectType $objectType = null; - private bool $isClassString; + private ?Type $arrayKeyType = null; - public function __construct(string $value, bool $isClassString = false) + /** @api */ + public function __construct(private string $value, private bool $isClassString = false) { - $this->value = $value; - $this->isClassString = $isClassString; + parent::__construct(); } public function getValue(): string @@ -45,34 +80,73 @@ public function getValue(): string return $this->value; } + public function getConstantStrings(): array + { + return [$this]; + } + + public function isClassString(): TrinaryLogic + { + if ($this->isClassString) { + return TrinaryLogic::createYes(); + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + + 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 { return $level->handle( - static function (): string { - return 'string'; - }, + 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}"; + } } - return var_export( - \Nette\Utils\Strings::truncate($this->value, self::DESCRIBE_LIMIT), - true - ); + return self::export($value); }, - function (): string { - return 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(); @@ -80,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. @@ -92,29 +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) { - $broker = Broker::getInstance(); - - return $broker->hasClass($this->getValue()) ? 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 @@ -123,26 +195,35 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' - if ($broker->hasFunction(new Name($this->value), null)) { + if ($reflectionProvider->hasFunction(new Name($this->value), null)) { return TrinaryLogic::createYes(); } // 'MyClass::myStaticFunction' - $matches = \Nette\Utils\Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); + $matches = Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); if ($matches !== null) { - if (!$broker->hasClass($matches[1])) { + if (!$reflectionProvider->hasClass($matches[1])) { return TrinaryLogic::createMaybe(); } - $classRef = $broker->getClass($matches[1]); + $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(); } @@ -152,49 +233,49 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - $broker = Broker::getInstance(); + if ($this->value === '') { + return []; + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' $functionName = new Name($this->value); - if ($broker->hasFunction($functionName, null)) { - return $broker->getFunction($functionName, null)->getVariants(); + if ($reflectionProvider->hasFunction($functionName, null)) { + $function = $reflectionProvider->getFunction($functionName, null); + return FunctionCallableVariant::createFromVariants($function, $function->getVariants()); } // 'MyClass::myStaticFunction' - $matches = \Nette\Utils\Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); + $matches = Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); if ($matches !== null) { - if (!$broker->hasClass($matches[1])) { + if (!$reflectionProvider->hasClass($matches[1])) { return [new TrivialParametersAcceptor()]; } - $classReflection = $broker->getClass($matches[1]); + $classReflection = $reflectionProvider->getClass($matches[1]); if ($classReflection->hasMethod($matches[2])) { $method = $classReflection->getMethod($matches[2], $scope); if (!$scope->canCallMethod($method)) { return [new InaccessibleMethod($method)]; } - return $method->getVariants(); + return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function toNumber(): Type { if (is_numeric($this->value)) { - /** @var mixed $value */ $value = $this->value; $value = +$value; if (is_float($value)) { @@ -207,32 +288,73 @@ public function toNumber(): Type return new ErrorType(); } - public function toInteger(): Type + public function toAbsoluteNumber(): Type { - $type = $this->toNumber(); - if ($type instanceof ErrorType) { - return $type; - } + return $this->toNumber()->toAbsoluteNumber(); + } - return $type->toInteger(); + public function toInteger(): Type + { + return new ConstantIntegerType((int) $this->value); } public function toFloat(): Type { - $type = $this->toNumber(); - if ($type instanceof ErrorType) { - return $type; + return new ConstantFloatType((float) $this->value); + } + + public function toArrayKey(): Type + { + if ($this->arrayKeyType !== null) { + return $this->arrayKeyType; } - return $type->toFloat(); + /** @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(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(is_numeric($this->getValue())); + } + + 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); @@ -240,18 +362,41 @@ 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); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { $valueStringType = $valueType->toString(); if ($valueStringType instanceof ErrorType) { @@ -262,7 +407,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType): Type && $valueStringType instanceof ConstantStringType ) { $value = $this->value; - $value[$offsetType->getValue()] = $valueStringType->getValue(); + $offsetValue = $offsetType->getValue(); + if ($offsetValue < 0) { + return new ErrorType(); + } + $stringValue = $valueStringType->getValue(); + if (strlen($stringValue) !== 1) { + return new ErrorType(); + } + $value[$offsetValue] = $stringValue; return new self($value); } @@ -270,26 +423,149 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType): Type 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()); } - public function generalize(): Type + public function generalize(GeneralizePrecision $precision): Type { if ($this->isClassString) { - return new ClassStringType(); + if ($precision->isMoreSpecific()) { + return new ClassStringType(); + } + + return new StringType(); + } + + if ($this->getValue() !== '' && $precision->isMoreSpecific()) { + $accessories = [ + new StringType(), + 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()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + return new StringType(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function getSmallerType(PhpVersion $phpVersion): Type { - return new self($properties['value'], $properties['isClassString'] ?? false); + $subtractedTypes = [ + new ConstantBooleanType(true), + IntegerRangeType::createAllGreaterThanOrEqualTo((float) $this->value), + ]; + + if ($this->value === '') { + $subtractedTypes[] = new NullType(); + $subtractedTypes[] = new StringType(); + } + + if (!(bool) $this->value) { + $subtractedTypes[] = new ConstantBooleanType(false); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + IntegerRangeType::createAllGreaterThan((float) $this->value), + ]; + + if (!(bool) $this->value) { + $subtractedTypes[] = new ConstantBooleanType(true); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new ConstantBooleanType(false), + IntegerRangeType::createAllSmallerThanOrEqualTo((float) $this->value), + ]; + + if ((bool) $this->value) { + $subtractedTypes[] = new ConstantBooleanType(true); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + IntegerRangeType::createAllSmallerThan((float) $this->value), + ]; + + if ((bool) $this->value) { + $subtractedTypes[] = new ConstantBooleanType(false); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function canAccessConstants(): TrinaryLogic + { + return $this->isClassString(); + } + + public function hasConstant(string $constantName): TrinaryLogic + { + 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 1e06686e75..b84b381717 100644 --- a/src/Type/ConstantScalarType.php +++ b/src/Type/ConstantScalarType.php @@ -2,7 +2,8 @@ namespace PHPStan\Type; -interface ConstantScalarType extends ConstantType +/** @api */ +interface ConstantScalarType extends Type { /** diff --git a/src/Type/ConstantType.php b/src/Type/ConstantType.php deleted file mode 100644 index e729d46760..0000000000 --- a/src/Type/ConstantType.php +++ /dev/null @@ -1,10 +0,0 @@ - ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $arrayBuilder->degradeToGeneralArray(true); + } foreach ($value as $k => $v) { $arrayBuilder->setOffsetValueType(self::getTypeFromValue($k), self::getTypeFromValue($v)); } return $arrayBuilder->getArray(); + } elseif (is_object($value)) { + $class = get_class($value); + /** phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName */ + if (function_exists('enum_exists') && \enum_exists($class)) { + /** @var UnitEnum $value */ + return new EnumCaseObjectType($class, $value->name); + } + /** phpcs:enable */ + + return new ObjectType(get_class($value)); } return new MixedType(); diff --git a/src/Type/DirectTypeAliasResolverProvider.php b/src/Type/DirectTypeAliasResolverProvider.php new file mode 100644 index 0000000000..f7fa61c09e --- /dev/null +++ b/src/Type/DirectTypeAliasResolverProvider.php @@ -0,0 +1,17 @@ +typeAliasResolver; + } + +} diff --git a/src/Type/DynamicFunctionReturnTypeExtension.php b/src/Type/DynamicFunctionReturnTypeExtension.php index aa0d2befcd..eb7b6222ff 100644 --- a/src/Type/DynamicFunctionReturnTypeExtension.php +++ b/src/Type/DynamicFunctionReturnTypeExtension.php @@ -6,11 +6,28 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +/** + * 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 new file mode 100644 index 0000000000..9e16865c3c --- /dev/null +++ b/src/Type/DynamicFunctionThrowTypeExtension.php @@ -0,0 +1,33 @@ +setBroker($broker); - } - - $this->reflectionProvider = $reflectionProvider; - $this->dynamicMethodReturnTypeExtensions = $dynamicMethodReturnTypeExtensions; - $this->dynamicStaticMethodReturnTypeExtensions = $dynamicStaticMethodReturnTypeExtensions; - $this->dynamicFunctionReturnTypeExtensions = $dynamicFunctionReturnTypeExtensions; } /** - * @param string $className - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] + * @return DynamicMethodReturnTypeExtension[] */ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array { if ($this->dynamicMethodReturnTypeExtensionsByClass === null) { $byClass = []; foreach ($this->dynamicMethodReturnTypeExtensions as $extension) { - $byClass[$extension->getClass()][] = $extension; + $byClass[strtolower($extension->getClass())][] = $extension; } $this->dynamicMethodReturnTypeExtensionsByClass = $byClass; @@ -73,15 +46,14 @@ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): } /** - * @param string $className - * @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] + * @return DynamicStaticMethodReturnTypeExtension[] */ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $className): array { if ($this->dynamicStaticMethodReturnTypeExtensionsByClass === null) { $byClass = []; foreach ($this->dynamicStaticMethodReturnTypeExtensions as $extension) { - $byClass[$extension->getClass()][] = $extension; + $byClass[strtolower($extension->getClass())][] = $extension; } $this->dynamicStaticMethodReturnTypeExtensionsByClass = $byClass; @@ -90,8 +62,7 @@ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $class } /** - * @param \PHPStan\Type\DynamicMethodReturnTypeExtension[][]|\PHPStan\Type\DynamicStaticMethodReturnTypeExtension[][] $extensions - * @param string $className + * @param DynamicMethodReturnTypeExtension[][]|DynamicStaticMethodReturnTypeExtension[][] $extensions * @return mixed[] */ private function getDynamicExtensionsForType(array $extensions, string $className): array @@ -103,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; } @@ -114,7 +86,7 @@ private function getDynamicExtensionsForType(array $extensions, string $classNam } /** - * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] + * @return DynamicFunctionReturnTypeExtension[] */ public function getDynamicFunctionReturnTypeExtensions(): array { diff --git a/src/Type/DynamicStaticMethodReturnTypeExtension.php b/src/Type/DynamicStaticMethodReturnTypeExtension.php index 4508b33ae2..e74f69460f 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -6,13 +6,31 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +/** + * 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 new file mode 100644 index 0000000000..fa9926dea3 --- /dev/null +++ b/src/Type/DynamicStaticMethodThrowTypeExtension.php @@ -0,0 +1,33 @@ +enumCaseName; + } + + public function describe(VerbosityLevel $level): string + { + $parent = parent::describe($level); + + return sprintf('%s::%s', $parent, $this->enumCaseName); + } + + public function equals(Type $type): bool + { + if (!$type instanceof self) { + return false; + } + + return $this->enumCaseName === $type->enumCaseName && + $this->getClassName() === $type->getClassName(); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return $this->isSuperTypeOf($type)->toAcceptsResult(); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof self) { + return IsSuperTypeOfResult::createFromBoolean( + $this->enumCaseName === $type->enumCaseName && $this->getClassName() === $type->getClassName(), + ); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + 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->changeSubtractedType($type); + } + + public function getTypeWithoutSubtractedType(): Type + { + return $this; + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + if ($subtractedType === null || ! $this->equals($subtractedType)) { + return $this; + } + + return new NeverType(); + } + + public function getSubtractedType(): ?Type + { + return null; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return parent::getUnresolvedPropertyPrototype($propertyName, $scope); + + } + if ($propertyName === 'name') { + return new EnumUnresolvedPropertyPrototypeReflection( + new EnumPropertyReflection($propertyName, $classReflection, new ConstantStringType($this->enumCaseName)), + ); + } + + if ($classReflection->isBackedEnum() && $propertyName === 'value') { + if ($classReflection->hasEnumCase($this->enumCaseName)) { + $enumCase = $classReflection->getEnumCase($this->enumCaseName); + $valueType = $enumCase->getBackingValueType(); + if ($valueType === null) { + throw new ShouldNotHappenException(); + } + + return new EnumUnresolvedPropertyPrototypeReflection( + new EnumPropertyReflection($propertyName, $classReflection, $valueType), + ); + } + } + + 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 + { + return new parent($this->getClassName(), null, $this->getClassReflection()); + } + + 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 ConstTypeNode( + new ConstFetchNode( + $this->getClassName(), + $this->getEnumCaseName(), + ), + ); + } + +} diff --git a/src/Type/ErrorType.php b/src/Type/ErrorType.php index 9c7416bd15..751271aef3 100644 --- a/src/Type/ErrorType.php +++ b/src/Type/ErrorType.php @@ -2,9 +2,11 @@ namespace PHPStan\Type; +/** @api */ class ErrorType extends MixedType { + /** @api */ public function __construct() { parent::__construct(); @@ -13,15 +15,9 @@ public function __construct() public function describe(VerbosityLevel $level): string { return $level->handle( - function () use ($level): string { - return parent::describe($level); - }, - function () use ($level): string { - return parent::describe($level); - }, - static function (): string { - return '*ERROR*'; - } + fn (): string => parent::describe($level), + fn (): string => parent::describe($level), + static fn (): string => '*ERROR*', ); } @@ -45,13 +41,4 @@ public function equals(Type $type): bool return $type instanceof self; } - /** - * @param mixed[] $properties - * @return Type - */ - 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 fe4814d0f4..d3f6af2137 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -2,138 +2,174 @@ namespace PHPStan\Type; +use Closure; use PhpParser\Node; use PHPStan\Analyser\NameScope; +use PHPStan\BetterReflection\Util\GetLastDocComment; use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Cache\Cache; +use PHPStan\File\FileHelper; use PHPStan\Parser\Parser; -use PHPStan\PhpDoc\NameScopedPhpDocString; +use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\Tag\TemplateTag; -use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; -use PHPStan\Type\Generic\TemplateType; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use function array_key_exists; -use function file_exists; -use function filemtime; - -class FileTypeMapper +use function array_keys; +use function array_map; +use function array_merge; +use function array_pop; +use function array_slice; +use function count; +use function is_array; +use function is_callable; +use function is_file; +use function ltrim; +use function md5; +use function sprintf; +use function str_contains; +use function strtolower; + +final class FileTypeMapper { private const SKIP_NODE = 1; private const POP_TYPE_MAP_STACK = 2; - private ReflectionProviderProvider $reflectionProviderProvider; - - private \PHPStan\Parser\Parser $phpParser; - - private \PHPStan\PhpDoc\PhpDocStringResolver $phpDocStringResolver; - - private \PHPStan\PhpDoc\PhpDocNodeResolver $phpDocNodeResolver; - - private \PHPStan\Cache\Cache $cache; - - private \PHPStan\Broker\AnonymousClassNameHelper $anonymousClassNameHelper; - - /** @var \PHPStan\PhpDoc\NameScopedPhpDocString[][] */ + /** @var NameScope[][] */ private array $memoryCache = []; - /** @var (false|(callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)|\PHPStan\PhpDoc\NameScopedPhpDocString)[][] */ + private int $memoryCacheCount = 0; + + /** @var (true|callable(): NameScope|NameScope)[][] */ private array $inProcess = []; /** @var array */ private array $resolvedPhpDocBlockCache = []; - /** @var array */ - private array $alreadyProcessedDependentFiles = []; + private int $resolvedPhpDocBlockCacheCount = 0; public function __construct( - ReflectionProviderProvider $reflectionProviderProvider, - Parser $phpParser, - PhpDocStringResolver $phpDocStringResolver, - PhpDocNodeResolver $phpDocNodeResolver, - Cache $cache, - AnonymousClassNameHelper $anonymousClassNameHelper + private ReflectionProviderProvider $reflectionProviderProvider, + private Parser $phpParser, + private PhpDocStringResolver $phpDocStringResolver, + private PhpDocNodeResolver $phpDocNodeResolver, + private AnonymousClassNameHelper $anonymousClassNameHelper, + private FileHelper $fileHelper, ) { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->phpParser = $phpParser; - $this->phpDocStringResolver = $phpDocStringResolver; - $this->phpDocNodeResolver = $phpDocNodeResolver; - $this->cache = $cache; - $this->anonymousClassNameHelper = $anonymousClassNameHelper; } + /** @api */ public function getResolvedPhpDoc( - string $fileName, + ?string $fileName, ?string $className, ?string $traitName, ?string $functionName, - string $docComment + string $docComment, ): ResolvedPhpDocBlock { if ($className === null && $traitName !== null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + if ($docComment === '') { + return ResolvedPhpDocBlock::createEmpty(); + } + + if ($fileName !== null) { + $fileName = $this->fileHelper->normalizePath($fileName); } - $phpDocKey = $this->getPhpDocKey($className, $traitName, $functionName, $docComment); + $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); + $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment)); if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) { return $this->resolvedPhpDocBlockCache[$phpDocKey]; } - $phpDocMap = []; + 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])) { - $phpDocMap = $this->getResolvedPhpDocMap($fileName); + $nameScopeMap = $this->getNameScopeMap($fileName); } - if (isset($phpDocMap[$phpDocKey])) { - return $this->createResolvedPhpDocBlock($phpDocKey, $phpDocMap[$phpDocKey], $fileName); + if (isset($nameScopeMap[$nameScopeKey])) { + return $nameScopeMap[$nameScopeKey]; } - if (!isset($this->inProcess[$fileName][$phpDocKey])) { // wrong $fileName due to traits - return ResolvedPhpDocBlock::createEmpty(); + if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits + throw new NameScopeAlreadyBeingCreatedException(); } - if ($this->inProcess[$fileName][$phpDocKey] === 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][$phpDocKey])) { - $resolveCallback = $this->inProcess[$fileName][$phpDocKey]; - $this->inProcess[$fileName][$phpDocKey] = false; - $this->inProcess[$fileName][$phpDocKey] = $resolveCallback(); + if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { + $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; + $this->inProcess[$fileName][$nameScopeKey] = true; + $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); } - assert($this->inProcess[$fileName][$phpDocKey] instanceof NameScopedPhpDocString); - return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$phpDocKey], $fileName); + return $this->inProcess[$fileName][$nameScopeKey]; } - private function createResolvedPhpDocBlock(string $phpDocKey, NameScopedPhpDocString $nameScopedPhpDocString, string $fileName): ResolvedPhpDocBlock + private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock { - $phpDocString = $nameScopedPhpDocString->getPhpDocString(); - $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); - $nameScope = $nameScopedPhpDocString->getNameScope(); - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); - $templateTypeScope = $nameScope->getTemplateTypeScope(); - - if ($templateTypeScope !== null) { - $templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type { - return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag); - }, $templateTags)); - $nameScope = $nameScope->withTemplateTypeMap( - new TemplateTypeMap(array_merge( - $nameScope->getTemplateTypeMap()->getTypes(), - $templateTypeMap->getTypes() - )) + $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); + if ($this->resolvedPhpDocBlockCacheCount >= 2048) { + $this->resolvedPhpDocBlockCache = array_slice( + $this->resolvedPhpDocBlockCache, + 1, + null, + true, ); - } else { - $templateTypeMap = TemplateTypeMap::createEmpty(); + + $this->resolvedPhpDocBlockCacheCount--; + } + + $templateTypeMap = $nameScope->getTemplateTypeMap(); + $phpDocTemplateTypes = []; + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); + foreach (array_keys($templateTags) as $name) { + $templateType = $templateTypeMap->getType($name); + if ($templateType === null) { + continue; + } + $phpDocTemplateTypes[$name] = $templateType; } $this->resolvedPhpDocBlockCache[$phpDocKey] = ResolvedPhpDocBlock::create( @@ -141,117 +177,73 @@ private function createResolvedPhpDocBlock(string $phpDocKey, NameScopedPhpDocSt $phpDocString, $fileName, $nameScope, - $templateTypeMap, + new TemplateTypeMap($phpDocTemplateTypes), $templateTags, - $this->phpDocNodeResolver + $this->phpDocNodeResolver, + $this->reflectionProviderProvider->getReflectionProvider(), ); + $this->resolvedPhpDocBlockCacheCount++; return $this->resolvedPhpDocBlockCache[$phpDocKey]; } - private function resolvePhpDocStringToDocNode(string $phpDocString): PhpDocNode - { - $phpDocParserVersion = 'Version unknown'; - try { - $phpDocParserVersion = \Jean85\PrettyVersions::getVersion('phpstan/phpdoc-parser')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { - // skip - } - $cacheKey = sprintf('phpdocstring-%s', $phpDocString); - $phpDocNodeSerializedString = $this->cache->load($cacheKey, $phpDocParserVersion); - if ($phpDocNodeSerializedString !== null) { - return unserialize($phpDocNodeSerializedString); - } - - $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); - if ($this->shouldPhpDocNodeBeCachedToDisk($phpDocNode)) { - $this->cache->save($cacheKey, $phpDocParserVersion, serialize($phpDocNode)); - } - - return $phpDocNode; - } - - private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool - { - foreach ($phpDocNode->getTags() as $phpDocTag) { - if (!$phpDocTag->value instanceof InvalidTagValueNode) { - continue; - } - - return false; - } - - return true; - } - /** - * @param string $fileName - * @return \PHPStan\PhpDoc\NameScopedPhpDocString[] + * @return NameScope[] */ - private function getResolvedPhpDocMap(string $fileName): array + private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { - $cacheKey = sprintf('%s-phpdocstring', $fileName); - $variableCacheKey = implode(',', array_map(static function (array $file): string { - return sprintf('%s-%d', $file['filename'], $file['modifiedTime']); - }, $this->getCachedDependentFilesWithTimestamps($fileName))); - $map = $this->cache->load($cacheKey, $variableCacheKey); - - if ($map === null) { - $map = $this->createResolvedPhpDocMap($fileName); - $this->cache->save($cacheKey, $variableCacheKey, $map); + $map = $this->createResolvedPhpDocMap($fileName); + if ($this->memoryCacheCount >= 2048) { + $this->memoryCache = array_slice( + $this->memoryCache, + 1, + null, + true, + ); + $this->memoryCacheCount--; } $this->memoryCache[$fileName] = $map; + $this->memoryCacheCount++; } return $this->memoryCache[$fileName]; } /** - * @param string $fileName - * @return \PHPStan\PhpDoc\NameScopedPhpDocString[] + * @return NameScope[] */ private function createResolvedPhpDocMap(string $fileName): array { - $phpDocMap = $this->createFilePhpDocMap($fileName, null, null); - $resolvedPhpDocMap = []; + $phpDocNodeMap = $this->createPhpDocNodeMap($fileName, null, $fileName, [], $fileName); + $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName, $phpDocNodeMap); + $resolvedNameScopeMap = []; try { - $this->inProcess[$fileName] = $phpDocMap; + $this->inProcess[$fileName] = $nameScopeMap; - foreach ($phpDocMap as $phpDocKey => $resolveCallback) { - $this->inProcess[$fileName][$phpDocKey] = false; - $this->inProcess[$fileName][$phpDocKey] = $data = $resolveCallback(); - $resolvedPhpDocMap[$phpDocKey] = $data; + foreach ($nameScopeMap as $nameScopeKey => $resolveCallback) { + $this->inProcess[$fileName][$nameScopeKey] = true; + $this->inProcess[$fileName][$nameScopeKey] = $data = $resolveCallback(); + $resolvedNameScopeMap[$nameScopeKey] = $data; } } finally { unset($this->inProcess[$fileName]); } - return $resolvedPhpDocMap; + return $resolvedNameScopeMap; } /** - * @param string $fileName - * @param string|null $lookForTrait - * @param string|null $traitUseClass * @param array $traitMethodAliases - * @return (callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)[] + * @return array */ - private function createFilePhpDocMap( - string $fileName, - ?string $lookForTrait, - ?string $traitUseClass, - array $traitMethodAliases = [] - ): array + private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array { - /** @var (callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)[] $phpDocMap */ - $phpDocMap = []; - - /** @var (callable(): TemplateTypeMap)[] $typeMapStack */ - $typeMapStack = []; + /** @var array $phpDocNodeMap */ + $phpDocNodeMap = []; /** @var string[] $classStack */ $classStack = []; @@ -259,36 +251,88 @@ private function createFilePhpDocMap( $classStack[] = $traitUseClass; } $namespace = null; - $functionName = null; - $uses = []; + + $traitFound = false; + + /** @var array $functionStack */ + $functionStack = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAliases, &$phpDocMap, &$classStack, &$namespace, &$functionName, &$uses, &$typeMapStack) { - $resolvableTemplateTypes = false; + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$phpDocNodeMap, &$classStack, &$namespace, &$functionStack): ?int { if ($node instanceof Node\Stmt\ClassLike) { - if ($lookForTrait !== null) { + if ($traitFound && $fileName === $originalClassFileName) { + return self::SKIP_NODE; + } + + if ($lookForTrait !== null && !$traitFound) { if (!$node instanceof Node\Stmt\Trait_) { return self::SKIP_NODE; } if ((string) $node->namespacedName !== $lookForTrait) { return self::SKIP_NODE; } + + $traitFound = true; + $functionStack[] = null; } else { if ($node->name === null) { if (!$node instanceof Node\Stmt\Class_) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); - } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + } 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; - $functionName = null; - $resolvableTemplateTypes = true; + $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) { @@ -296,15 +340,21 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia 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; } foreach ($node->traits as $traitName) { @@ -319,173 +369,451 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia if (!$traitReflection->isTrait()) { continue; } - if ($traitReflection->getFileName() === false) { + if ($traitReflection->getFileName() === null) { continue; } - if (!file_exists($traitReflection->getFileName())) { + if (!is_file($traitReflection->getFileName())) { continue; } $className = $classStack[count($classStack) - 1] ?? null; if ($className === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $traitPhpDocMap = $this->createFilePhpDocMap( + $phpDocNodeMap = array_merge($phpDocNodeMap, $this->createPhpDocNodeMap( $traitReflection->getFileName(), $traitName, $className, - $traitMethodAliases[$traitName] ?? [] - ); - $phpDocMap = array_merge($phpDocMap, $traitPhpDocMap); + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, + )); } - return null; - } elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) { - $namespace = (string) $node->name; - return null; - } elseif ($node instanceof \PhpParser\Node\Stmt\Use_ && $node->type === \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) { - foreach ($node->uses as $use) { - $uses[strtolower($use->getAlias()->name)] = (string) $use->name; + } + + return null; + }, + static function (Node $node) use (&$namespace, &$functionStack, &$classStack): void { + if ($node instanceof Node\Stmt\ClassLike) { + if (count($classStack) === 0) { + throw new ShouldNotHappenException(); } - return null; - } elseif ($node instanceof \PhpParser\Node\Stmt\GroupUse) { - $prefix = (string) $node->prefix; - foreach ($node->uses as $use) { - if ($node->type !== \PhpParser\Node\Stmt\Use_::TYPE_NORMAL && $use->type !== \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) { - continue; + 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(); } - $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); + array_pop($functionStack); + } + } + }, + ); + + return $phpDocNodeMap; + } + + /** + * @param array $traitMethodAliases + * @param array $phpDocNodeMap + * @return (callable(): NameScope)[] + */ + private function createNameScopeMap( + string $fileName, + ?string $lookForTrait, + ?string $traitUseClass, + array $traitMethodAliases, + string $originalClassFileName, + array $phpDocNodeMap, + ): array + { + /** @var (callable(): NameScope)[] $nameScopeMap */ + $nameScopeMap = []; + + /** @var (callable(): TemplateTypeMap)[] $typeMapStack */ + $typeMapStack = []; + + /** @var array> $typeAliasStack */ + $typeAliasStack = []; + + /** @var string[] $classStack */ + $classStack = []; + if ($lookForTrait !== null && $traitUseClass !== null) { + $classStack[] = $traitUseClass; + $typeAliasStack[] = []; + } + $namespace = null; + + $traitFound = false; + + /** @var array $functionStack */ + $functionStack = []; + $uses = []; + $constUses = []; + $this->processNodes( + $this->phpParser->parseFile($fileName), + 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; + } + + 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; + $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_) { + 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; + $classNameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, null); + if (array_key_exists($classNameScopeKey, $phpDocNodeMap)) { + $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$classNameScopeKey]); + } else { + $typeAliasStack[] = []; + } + $functionStack[] = null; } - return null; } elseif ($node instanceof Node\Stmt\ClassMethod) { - $functionName = $node->name->name; - if (array_key_exists($functionName, $traitMethodAliases)) { - $functionName = $traitMethodAliases[$functionName]; + if (array_key_exists($node->name->name, $traitMethodAliases)) { + $functionStack[] = $traitMethodAliases[$node->name->name]; + } else { + $functionStack[] = $node->name->name; } - $resolvableTemplateTypes = true; } elseif ($node instanceof Node\Stmt\Function_) { - $functionName = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); - $resolvableTemplateTypes = true; - } elseif ($node instanceof Node\Stmt\Property) { - $resolvableTemplateTypes = true; - } elseif (!in_array(get_class($node), [ - Node\Stmt\Foreach_::class, - Node\Expr\Assign::class, - Node\Expr\AssignRef::class, - Node\Stmt\Class_::class, - Node\Stmt\ClassConst::class, - Node\Stmt\Static_::class, - Node\Stmt\Echo_::class, - Node\Stmt\Return_::class, - Node\Stmt\Expression::class, - Node\Stmt\Throw_::class, - Node\Stmt\If_::class, - Node\Stmt\While_::class, - Node\Stmt\Switch_::class, - Node\Stmt\Nop::class, - ], true)) { - return null; + $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()); + } } - $phpDocString = CommentHelper::getDocComment($node); - if ($phpDocString === null) { - return null; + $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_) { + // 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; + $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) { + throw new ShouldNotHappenException(); + } + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + + return new TemplateTypeMap(array_merge( + $currentTypeMap !== null ? $currentTypeMap->getTypes() : [], + $templateTypeMap->getTypes(), + )); + }; + } } - $className = $classStack[count($classStack) - 1] ?? null; $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; - - $phpDocKey = $this->getPhpDocKey($className, $lookForTrait, $functionName, $phpDocString); - $phpDocMap[$phpDocKey] = static function () use ($phpDocString, $namespace, $uses, $className, $functionName, $typeMapCb, $resolvableTemplateTypes): NameScopedPhpDocString { - $nameScope = new NameScope( + $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; + + if ( + ( + $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, $uses, $className, $functionName, - ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty())->map(static function (string $name, Type $type) use ($className, $resolvableTemplateTypes): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($className, $resolvableTemplateTypes): Type { - if (!$type instanceof TemplateType) { - return $traverse($type); - } + ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()), + $typeAliasesMap, + false, + $constUses, + $lookForTrait, + ); + } - if (!$resolvableTemplateTypes) { - return $traverse($type->toArgument()); - } + if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + // property hook skipped on purpose, it does not support @template + if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { + return self::POP_TYPE_MAP_STACK; + } - $scope = $type->getScope(); + return null; + } - if ($scope->getClassName() === null || $scope->getFunctionName() !== null || $scope->getClassName() !== $className) { - return $traverse($type->toArgument()); + if ($node instanceof Node\Stmt\Namespace_) { + $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) { + $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); + } + } + } 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; + } + + $useDocComment = null; + if ($node->getDocComment() !== null) { + $useDocComment = $node->getDocComment()->getText(); + } + + 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(); + } + + $traitPhpDocMap = $this->createNameScopeMap( + $traitReflection->getFileName(), + $traitName, + $className, + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, + $phpDocNodeMap, + ); + $finalTraitPhpDocMap = []; + foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) { + $finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScope { + /** @var NameScope $original */ + $original = $callback(); + if (!$traitReflection->isGeneric()) { + return $original; } - return $traverse($type); - }); - }) - ); - return new NameScopedPhpDocString($phpDocString, $nameScope); - }; + $traitTemplateTypeMap = $traitReflection->getTemplateTypeMap(); + + $useType = null; + if ($useDocComment !== null) { + $useTags = $this->getResolvedPhpDoc( + $fileName, + $className, + $lookForTrait, + null, + $useDocComment, + )->getUsesTags(); + foreach ($useTags as $useTag) { + $useTagType = $useTag->getType(); + if (!$useTagType instanceof GenericObjectType) { + continue; + } + + if ($useTagType->getClassName() !== $traitReflection->getName()) { + continue; + } + + $useType = $useTagType; + break; + } + } - if (!($node instanceof Node\Stmt\ClassLike) && !($node instanceof Node\FunctionLike)) { - return null; - } + if ($useType === null) { + return $original->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds()); + } + + $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes()); - $typeMapStack[] = function () use ($fileName, $className, $lookForTrait, $functionName, $phpDocString, $typeMapCb): TemplateTypeMap { - static $typeMap = null; - if ($typeMap !== null) { - return $typeMap; + 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); } - $resolvedPhpDoc = $this->getResolvedPhpDoc( - $fileName, - $className, - $lookForTrait, - $functionName, - $phpDocString - ); - return new TemplateTypeMap(array_merge( - $typeMapCb !== null ? $typeMapCb()->getTypes() : [], - $resolvedPhpDoc->getTemplateTypeMap()->getTypes() - )); - }; + } - return self::POP_TYPE_MAP_STACK; + return null; }, - static function (\PhpParser\Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionName, &$classStack, &$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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($classStack); - } elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) { + + if (count($typeAliasStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($typeAliasStack); + + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } elseif ($node instanceof Node\Stmt\Namespace_) { $namespace = null; $uses = []; + $constUses = []; } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { - $functionName = null; + 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; } if (count($typeMapStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($typeMapStack); - } + }, ); if (count($typeMapStack) > 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + return $nameScopeMap; + } + + /** + * @return array + */ + private function getTypeAliasesMap(PhpDocNode $phpDocNode): array + { + $nameScope = new NameScope(null, []); + + $aliasesMap = []; + foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasImportTags($phpDocNode, $nameScope)) as $key) { + $aliasesMap[$key] = true; + } + + foreach (array_keys($this->phpDocNodeResolver->resolveTypeAliasTags($phpDocNode, $nameScope)) as $key) { + $aliasesMap[$key] = true; } - return $phpDocMap; + return $aliasesMap; } /** - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param \Closure(\PhpParser\Node $node): mixed $nodeCallback - * @param \Closure(\PhpParser\Node $node, mixed $callbackResult): void $endNodeCallback + * @param Node[]|Node|scalar|null $node + * @param Closure(Node $node): mixed $nodeCallback + * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback */ - private function processNodes($node, \Closure $nodeCallback, \Closure $endNodeCallback): void + private function processNodes($node, Closure $nodeCallback, Closure $endNodeCallback): void { if ($node instanceof Node) { $callbackResult = $nodeCallback($node); @@ -504,138 +832,22 @@ private function processNodes($node, \Closure $nodeCallback, \Closure $endNodeCa } } - private function getPhpDocKey( + private function getNameScopeKey( + ?string $file, ?string $class, ?string $trait, ?string $function, - string $docComment ): string { - $docComment = \Nette\Utils\Strings::replace($docComment, '#\s+#', ' '); - - return md5(sprintf('%s-%s-%s-%s', $class, $trait, $function, $docComment)); - } - - /** - * @param string $fileName - * @return array - */ - private function getCachedDependentFilesWithTimestamps(string $fileName): array - { - $cacheKey = sprintf('dependentFilesTimestamps-%s', $fileName); - $fileModifiedTime = filemtime($fileName); - if ($fileModifiedTime === false) { - $fileModifiedTime = time(); - } - $variableCacheKey = sprintf('%d', $fileModifiedTime); - /** @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 (!file_exists($cachedFilename)) { - $useCached = false; - break; - } - - $currentTimestamp = filemtime($cachedFilename); - if ($currentTimestamp === false) { - $useCached = false; - break; - } - - if ($currentTimestamp !== $cachedTimestamp) { - $useCached = false; - break; - } - } - - if ($useCached) { - return $cachedFilesTimestamps; - } + if ($class === null && $trait === null && $function === null) { + return md5(sprintf('%s', $file ?? 'no-file')); } - $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; - } - - /** - * @param string $fileName - * @return string[] - */ - private function getDependentFiles(string $fileName): array - { - $dependentFiles = [$fileName]; - - if (isset($this->alreadyProcessedDependentFiles[$fileName])) { - return $dependentFiles; + if ($class !== null && str_contains($class, 'class@anonymous')) { + throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().'); } - $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_) { - 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 (!file_exists($traitReflection->getFileName())) { - continue; - } - - foreach ($this->getDependentFiles($traitReflection->getFileName()) as $traitFileName) { - $dependentFiles[] = $traitFileName; - } - } - } - }, - 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 10bfc54403..e38e5be35a 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -2,64 +2,95 @@ 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; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function get_class; +/** @api */ class FloatType implements Type { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; + use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + 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 getConstantStrings(): array + { + return []; + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof self || $type instanceof IntegerType) { - return TrinaryLogic::createYes(); + 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 { - return $type instanceof self; + return get_class($type) === static::class; } public function describe(VerbosityLevel $level): string @@ -72,6 +103,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toFloat(): Type { return $this; @@ -84,7 +120,11 @@ public function toInteger(): Type public function toString(): Type { - return new StringType(); + return new IntersectionType([ + new StringType(), + new AccessoryUppercaseStringType(), + new AccessoryNumericStringType(), + ]); } public function toArray(): Type @@ -92,47 +132,169 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + [], + TrinaryLogic::createYes(), ); } - public function isOffsetAccessible(): 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 hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type + 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 setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function getObjectTypeOrClassStringObjectType(): Type { return new ErrorType(); } - public function isArray(): TrinaryLogic + 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 - * @return Type - */ - 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 self(); + 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 @@ +value === self::LESS_SPECIFIC; + } + + public function isMoreSpecific(): bool + { + return $this->value === self::MORE_SPECIFIC; + } + + public function isTemplateArgument(): bool + { + return $this->value === self::TEMPLATE_ARGUMENT; + } + +} diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index be4742974c..4e04a9df28 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -2,13 +2,18 @@ namespace PHPStan\Type\Generic; -use PHPStan\Broker\Broker; -use PHPStan\TrinaryLogic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; 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\StaticType; @@ -17,15 +22,17 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; +/** @api */ class GenericClassStringType extends ClassStringType { - private Type $type; - - public function __construct(Type $type) + /** @api */ + public function __construct(private Type $type) { - $this->type = $type; + parent::__construct(); } public function getReferencedClasses(): array @@ -38,21 +45,30 @@ public function getGenericType(): Type return $this->type; } + public function getClassStringObjectType(): Type + { + return $this->getGenericType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->getClassStringObjectType(); + } + public function describe(VerbosityLevel $level): string { return sprintf('%s<%s>', parent::describe($level), $this->type->describe($level)); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ConstantStringType) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($type->getValue())) { - return TrinaryLogic::createNo(); + if (!$type->isClassString()->yes()) { + return AcceptsResult::createNo(); } $objectType = new ObjectType($type->getValue()); @@ -61,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); @@ -78,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) { @@ -97,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 @@ -121,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) { @@ -131,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(); @@ -142,11 +168,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - if (!$this->type->isSuperTypeOf($typeToInfer)->no()) { - return $this->type->inferTemplateTypes($typeToInfer); - } - - return TemplateTypeMap::createEmpty(); + return $this->type->inferTemplateTypes($typeToInfer); } public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array @@ -156,13 +178,48 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return $this->type->getReferencedTemplateTypes($variance); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function equals(Type $type): bool + { + if (!$type instanceof self) { + return false; + } + + if (!parent::equals($type)) { + return false; + } + + if (!$this->type->equals($type->type)) { + return false; + } + + return true; + } + + 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 9dbeff7b2a..a2bdadd7ae 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,44 +2,50 @@ namespace PHPStan\Type\Generic; -use PHPStan\Broker\Broker; +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\ResolvedMethodReflection; -use PHPStan\Reflection\ResolvedPropertyReflection; -use PHPStan\TrinaryLogic; +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\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; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function count; +use function implode; +use function sprintf; -final class GenericObjectType extends ObjectType +/** @api */ +class GenericObjectType extends ObjectType { - /** @var array */ - private array $types; - - private ?ClassReflection $classReflection; - /** + * @api * @param array $types + * @param array $variances */ public function __construct( string $mainType, - array $types, + private array $types, ?Type $subtractedType = null, - ?ClassReflection $classReflection = null + private ?ClassReflection $classReflection = null, + private array $variances = [], ) { parent::__construct($mainType, $subtractedType, $classReflection); - $this->types = $types; - $this->classReflection = $classReflection; } public function describe(VerbosityLevel $level): string @@ -47,9 +53,11 @@ public function describe(VerbosityLevel $level): string return sprintf( '%s<%s>', parent::describe($level), - implode(', ', array_map(static function (Type $type) use ($level): string { - return $type->describe($level); - }, $this->types)) + implode(', ', array_map( + static fn (Type $type, ?TemplateTypeVariance $variance = null): string => TypeProjectionHelper::describe($type, $variance, $level), + $this->types, + $this->variances, + )), ); } @@ -72,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(); @@ -98,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; @@ -136,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(); @@ -149,6 +166,7 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar } $typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); + $results = []; foreach ($typeList as $i => $templateType) { if (!isset($ancestor->types[$i])) { continue; @@ -160,14 +178,30 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar continue; } if (!$templateType instanceof TemplateType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!$templateType->isValidVariance($this->types[$i], $ancestor->types[$i])) { - return TrinaryLogic::createNo(); + + $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)); } - return $nakedSuperTypeOf; + if (count($results) === 0) { + return $nakedSuperTypeOf; + } + + $result = IsSuperTypeOfResult::createYes(); + foreach ($results as $innerResult) { + $result = $result->and($innerResult); + } + + return $result; } public function getClassReflection(): ?ClassReflection @@ -176,32 +210,38 @@ public function getClassReflection(): ?ClassReflection return $this->classReflection; } - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->getClassName())) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->getClassName())) { return null; } - return $this->classReflection = $broker->getClass($this->getClassName())->withTypes($this->types); + return $this->classReflection = $reflectionProvider->getClass($this->getClassName()) + ->withTypes($this->types) + ->withVariances($this->variances); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - $reflection = parent::getProperty($propertyName, $scope); + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } - return new ResolvedPropertyReflection( - $reflection, - $this->getClassReflection()->getActiveTemplateTypeMap() - ); + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $prototype = parent::getUnresolvedPropertyPrototype($propertyName, $scope); + + return $prototype->doNotResolveTemplateTypeMapToBounds(); + } + + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection + { + return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - $reflection = parent::getMethod($methodName, $scope); + $prototype = parent::getUnresolvedMethodPrototype($methodName, $scope); - return new ResolvedMethodReflection( - $reflection, - $this->getClassReflection()->getActiveTemplateTypeMap() - ); + return $prototype->doNotResolveTemplateTypeMapToBounds(); } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -216,11 +256,15 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $ancestor = $receivedType->getAncestorWithClassName($this->getClassName()); - if ($ancestor === null || !$ancestor instanceof GenericObjectType) { + if ($ancestor === null) { + return TemplateTypeMap::createEmpty(); + } + $ancestorClassReflection = $ancestor->getClassReflection(); + if ($ancestorClassReflection === null) { return TemplateTypeMap::createEmpty(); } - $otherTypes = $ancestor->getTypes(); + $otherTypes = $ancestorClassReflection->typeMapToList($ancestorClassReflection->getActiveTemplateTypeMap()); $typeMap = TemplateTypeMap::createEmpty(); foreach ($this->getTypes() as $i => $type) { @@ -243,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; } @@ -273,31 +318,75 @@ public function traverse(callable $cb): Type } if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { - return new static( - $this->getClassName(), - $types, - $subtractedType - ); + return $this->recreate($this->getClassName(), $types, $subtractedType, $this->variances); } return $this; } - public function changeSubtractedType(?Type $subtractedType): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - return new self($this->getClassName(), $this->types, $subtractedType); + 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; } /** - * @param mixed[] $properties - * @return Type + * @param Type[] $types + * @param TemplateTypeVariance[] $variances */ - public static function __set_state(array $properties): Type + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): self { return new self( - $properties['className'], - $properties['types'], - $properties['subtractedType'] ?? null + $className, + $types, + $subtractedType, + null, + $variances, + ); + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances); + } + + public function toPhpDocNode(): TypeNode + { + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), ); } 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 new file mode 100644 index 0000000000..e6658632cd --- /dev/null +++ b/src/Type/Generic/TemplateArrayType.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, + ArrayType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getKeyType(), $bound->getItemType()); + $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/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php new file mode 100644 index 0000000000..aea8573131 --- /dev/null +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -0,0 +1,67 @@ + */ + 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()); + + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + /** @param Type[] $types */ + public function withTypes(array $types): self + { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + new BenevolentUnionType($types), + $this->default, + ); + } + + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + return $result; + } + +} diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php new file mode 100644 index 0000000000..27fc50f21b --- /dev/null +++ b/src/Type/Generic/TemplateBooleanType.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, + BooleanType $bound, + ?Type $default, + ) + { + parent::__construct(); + $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/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php new file mode 100644 index 0000000000..53ea994935 --- /dev/null +++ b/src/Type/Generic/TemplateConstantArrayType.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, + ConstantArrayType $bound, + ?Type $default, + ) + { + 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; + $this->default = $default; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} 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 new file mode 100644 index 0000000000..b3df6ccd2e --- /dev/null +++ b/src/Type/Generic/TemplateFloatType.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, + FloatType $bound, + ?Type $default, + ) + { + parent::__construct(); + $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/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php new file mode 100644 index 0000000000..0c58b3b41e --- /dev/null +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -0,0 +1,50 @@ + */ + 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(), null, null, $bound->getVariances()); + + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType + { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $this->getBound(), + $this->default, + ); + } + +} diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php new file mode 100644 index 0000000000..b4057fa327 --- /dev/null +++ b/src/Type/Generic/TemplateIntegerType.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, + IntegerType $bound, + ?Type $default, + ) + { + parent::__construct(); + $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/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 27adda72d5..8160633fc3 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -2,33 +2,29 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; -use PHPStan\Type\IntersectionType; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; +/** @api */ final class TemplateMixedType extends MixedType implements TemplateType { - private TemplateTypeScope $scope; - - private string $name; - - private TemplateTypeStrategy $strategy; - - private TemplateTypeVariance $variance; - - private ?MixedType $bound = null; + /** @use TemplateTypeTrait */ + use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, - string $name + string $name, + MixedType $bound, + ?Type $default, ) { parent::__construct(true); @@ -37,166 +33,33 @@ public function __construct( $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; $this->name = $name; + $this->bound = $bound; + $this->default = $default; } - public function getName(): string - { - return $this->name; - } - - public function getScope(): TemplateTypeScope - { - return $this->scope; - } - - public function describe(VerbosityLevel $level): string - { - $basicDescription = function (): string { - return $this->name; - }; - return $level->handle( - $basicDescription, - $basicDescription, - function () use ($basicDescription): string { - return sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'); - } - ); - } - - public function equals(Type $type): bool - { - return $type instanceof self - && $type->scope->equals($this->scope) - && $type->name === $this->name - && parent::equals($type); - } - - public function getBound(): Type - { - if ($this->bound === null) { - $this->bound = new MixedType(true); - } - return $this->bound; - } - - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->strategy->accepts($this, $type, $strictTypes); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); - } - - return $this->getBound()->isSuperTypeOf($type) - ->and(TrinaryLogic::createMaybe()); - } - - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult { return $this->isSuperTypeOf($type); } - public function isSubTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $type->isSuperTypeOf($this); - } - - if (!$type instanceof TemplateType) { - return $type->isSuperTypeOf($this->getBound()); - } - - if ($this->equals($type)) { - return TrinaryLogic::createYes(); - } - - return $type->getBound()->isSuperTypeOf($this->getBound()) - ->and(TrinaryLogic::createMaybe()); - } - - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { - return $receivedType->inferTemplateTypesOn($this); + $isSuperType = $this->isSuperTypeOf($acceptingType)->toAcceptsResult(); + if ($isSuperType->no()) { + return $isSuperType; } - - if ( - $receivedType instanceof TemplateType - && $this->getBound()->isSuperTypeOf($receivedType->getBound())->yes() - ) { - return new TemplateTypeMap([ - $this->name => $receivedType, - ]); - } - - if ($this->getBound()->isSuperTypeOf($receivedType)->yes()) { - return new TemplateTypeMap([ - $this->name => TypeUtils::generalizeType($receivedType), - ]); - } - - return TemplateTypeMap::createEmpty(); + return AcceptsResult::createYes(); } - public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + public function toStrictMixedType(): TemplateStrictMixedType { - return [new TemplateTypeReference($this, $positionVariance)]; - } - - public function isArgument(): bool - { - return $this->strategy->isArgument(); - } - - public function toArgument(): TemplateType - { - return new self( + return new TemplateStrictMixedType( $this->scope, - new TemplateTypeArgumentStrategy(), + $this->strategy, $this->variance, - $this->name - ); - } - - public function isValidVariance(Type $a, Type $b): bool - { - return $this->variance->isValidVariance($a, $b); - } - - public function subtract(Type $type): Type - { - return $this; - } - - public function getTypeWithoutSubtractedType(): Type - { - return $this; - } - - public function changeSubtractedType(?Type $subtractedType): Type - { - return $this; - } - - public function getVariance(): TemplateTypeVariance - { - return $this->variance; - } - - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['scope'], - $properties['strategy'], - $properties['variance'], - $properties['name'] + $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 5ff8d094d7..220414ca14 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -2,217 +2,38 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; +/** @api */ final class TemplateObjectType extends ObjectType implements TemplateType { - private TemplateTypeScope $scope; - - private string $name; - - private TemplateTypeStrategy $strategy; - - private ObjectType $bound; - - private TemplateTypeVariance $variance; + use UndecidedComparisonCompoundTypeTrait; + /** @use TemplateTypeTrait */ + use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - string $class + ObjectType $bound, + ?Type $default, ) { - parent::__construct($class); + parent::__construct($bound->getClassName()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; $this->name = $name; - $this->bound = new ObjectType($class); - } - - public function getName(): string - { - return $this->name; - } - - public function getScope(): TemplateTypeScope - { - return $this->scope; - } - - public function describe(VerbosityLevel $level): string - { - $basicDescription = function () use ($level): string { - return sprintf( - '%s of %s', - $this->name, - parent::describe($level) - ); - }; - - return $level->handle( - $basicDescription, - $basicDescription, - function () use ($basicDescription): string { - return sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'); - } - ); - } - - public function equals(Type $type): bool - { - return $type instanceof self - && $type->scope->equals($this->scope) - && $type->name === $this->name - && parent::equals($type); - } - - public function getBound(): Type - { - return $this->bound; - } - - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->strategy->accepts($this, $type, $strictTypes); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); - } - - return $this->getBound()->isSuperTypeOf($type) - ->and(TrinaryLogic::createMaybe()); - } - - public function isSubTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $type->isSuperTypeOf($this); - } - - if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createYes(); - } - - if (!$type instanceof TemplateType) { - return $type->isSuperTypeOf($this->getBound()); - } - - if ($this->equals($type)) { - return TrinaryLogic::createYes(); - } - - if ($type->getBound()->isSuperTypeOf($this->getBound())->no() && - $this->getBound()->isSuperTypeOf($type->getBound())->no()) { - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createMaybe(); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isSubTypeOf($acceptingType); - } - - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap - { - if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { - return $receivedType->inferTemplateTypesOn($this); - } - - if ( - $receivedType instanceof TemplateType - && $this->getBound()->isSuperTypeOf($receivedType->getBound())->yes() - ) { - return new TemplateTypeMap([ - $this->name => $receivedType, - ]); - } - - if ($this->getBound()->isSuperTypeOf($receivedType)->yes()) { - return new TemplateTypeMap([ - $this->name => TypeUtils::generalizeType($receivedType), - ]); - } - - return TemplateTypeMap::createEmpty(); - } - - public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array - { - return [new TemplateTypeReference($this, $positionVariance)]; - } - - public function isArgument(): bool - { - return $this->strategy->isArgument(); - } - - public function toArgument(): TemplateType - { - return new self( - $this->scope, - new TemplateTypeArgumentStrategy(), - TemplateTypeVariance::createInvariant(), - $this->name, - $this->getClassName() - ); - } - - public function isValidVariance(Type $a, Type $b): bool - { - return $this->variance->isValidVariance($a, $b); - } - - public function subtract(Type $type): Type - { - return $this; - } - - public function getTypeWithoutSubtractedType(): Type - { - return $this; - } - - public function changeSubtractedType(?Type $subtractedType): Type - { - return $this; - } - - public function getVariance(): TemplateTypeVariance - { - return $this->variance; - } - - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['scope'], - $properties['strategy'], - $properties['variance'], - $properties['name'], - $properties['className'] - ); + $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 88ef60dee7..7d6aebc6f9 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -2,33 +2,28 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; +/** @api */ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements TemplateType { - private TemplateTypeScope $scope; - - private string $name; - - private TemplateTypeStrategy $strategy; - - private TemplateTypeVariance $variance; - - private ?ObjectWithoutClassType $bound = null; + use UndecidedComparisonCompoundTypeTrait; + /** @use TemplateTypeTrait */ + use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, - string $name + string $name, + ObjectWithoutClassType $bound, + ?Type $default, ) { parent::__construct(); @@ -37,173 +32,8 @@ public function __construct( $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; $this->name = $name; - } - - public function getName(): string - { - return $this->name; - } - - public function getScope(): TemplateTypeScope - { - return $this->scope; - } - - public function getBound(): Type - { - if ($this->bound === null) { - $this->bound = new ObjectWithoutClassType(); - } - return $this->bound; - } - - public function describe(VerbosityLevel $level): string - { - $basicDescription = function () use ($level): string { - return sprintf( - '%s of %s', - $this->name, - parent::describe($level) - ); - }; - - return $level->handle( - $basicDescription, - $basicDescription, - function () use ($basicDescription): string { - return sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'); - } - ); - } - - public function isArgument(): bool - { - return $this->strategy->isArgument(); - } - - public function toArgument(): TemplateType - { - return new self( - $this->scope, - new TemplateTypeArgumentStrategy(), - $this->variance, - $this->name - ); - } - - public function isValidVariance(Type $a, Type $b): bool - { - return $this->variance->isValidVariance($a, $b); - } - - public function subtract(Type $type): Type - { - return $this; - } - - public function getTypeWithoutSubtractedType(): Type - { - return $this; - } - - public function changeSubtractedType(?Type $subtractedType): Type - { - return $this; - } - - public function equals(Type $type): bool - { - return $type instanceof self - && $type->scope->equals($this->scope) - && $type->name === $this->name - && parent::equals($type); - } - - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic - { - return $this->isSubTypeOf($acceptingType); - } - - public function accepts(Type $type, bool $strictTypes): TrinaryLogic - { - return $this->strategy->accepts($this, $type, $strictTypes); - } - - public function isSubTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $type->isSuperTypeOf($this); - } - - if (!$type instanceof TemplateType) { - return $type->isSuperTypeOf($this->getBound()); - } - - if ($this->equals($type)) { - return TrinaryLogic::createYes(); - } - - return $type->getBound()->isSuperTypeOf($this->getBound()) - ->and(TrinaryLogic::createMaybe()); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); - } - - return $this->getBound()->isSuperTypeOf($type) - ->and(TrinaryLogic::createMaybe()); - } - - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap - { - if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { - return $receivedType->inferTemplateTypesOn($this); - } - - if ( - $receivedType instanceof TemplateType - && $this->getBound()->isSuperTypeOf($receivedType->getBound())->yes() - ) { - return new TemplateTypeMap([ - $this->name => $receivedType, - ]); - } - - if ($this->getBound()->isSuperTypeOf($receivedType)->yes()) { - return new TemplateTypeMap([ - $this->name => TypeUtils::generalizeType($receivedType), - ]); - } - - return TemplateTypeMap::createEmpty(); - } - - - public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array - { - return [new TemplateTypeReference($this, $positionVariance)]; - } - - public function getVariance(): TemplateTypeVariance - { - return $this->variance; - } - - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type - { - return new self( - $properties['scope'], - $properties['strategy'], - $properties['variance'], - $properties['name'] - ); + $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php new file mode 100644 index 0000000000..6feefd8696 --- /dev/null +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -0,0 +1,48 @@ + */ + 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; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + public function isSuperTypeOfMixed(MixedType $type): IsSuperTypeOfResult + { + return $this->isSuperTypeOf($type); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + +} diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php new file mode 100644 index 0000000000..1ae72a3384 --- /dev/null +++ b/src/Type/Generic/TemplateStringType.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, + StringType $bound, + ?Type $default, + ) + { + parent::__construct(); + $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/TemplateType.php b/src/Type/Generic/TemplateType.php index 9db42c1c3e..2dfa5dafbd 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -3,23 +3,30 @@ namespace PHPStan\Type\Generic; 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): bool; + 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 07d1cc35e5..0e8f59cf5e 100644 --- a/src/Type/Generic/TemplateTypeArgumentStrategy.php +++ b/src/Type/Generic/TemplateTypeArgumentStrategy.php @@ -2,31 +2,40 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\CompoundType; use PHPStan\Type\Type; +use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** * Template type strategy suitable for return type acceptance contexts */ -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(); - } + if ($right instanceof CompoundType) { + $accepts = $right->isAcceptedBy($left, $strictTypes); + } else { + $accepts = $left->getBound()->accepts($right, $strictTypes) + ->and(AcceptsResult::createMaybe()); + if ($accepts->maybe()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($left, $right); + + return new AcceptsResult($accepts->result, array_merge($accepts->reasons, [ + sprintf( + 'Type %s is not always the same as %s. It breaks the contract for some argument types, typically subtypes.', + $right->describe($verbosity), + $left->getName(), + ), + ])); } - - return TrinaryLogic::createNo(); } - return TrinaryLogic::createFromBoolean($left->equals($right)) - ->or(TrinaryLogic::createFromBoolean($right->equals(new MixedType()))); + return $accepts; } public function isArgument(): bool @@ -34,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 2674999fbf..8f56cc6cbb 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -3,41 +3,116 @@ namespace PHPStan\Type\Generic; use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\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; use PHPStan\Type\Type; +use PHPStan\Type\UnionType; +use function get_class; final class TemplateTypeFactory { - public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance): Type + /** + * @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); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } - if ($bound instanceof ObjectType) { - return new TemplateObjectType($scope, $strategy, $variance, $name, $bound->getClassName()); + $boundClass = get_class($bound); + if ($bound instanceof ObjectType && ($boundClass === ObjectType::class || $bound instanceof TemplateType)) { + return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default); } - $boundClass = get_class($bound); - if ($boundClass === ObjectWithoutClassType::class) { - return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name); + if ($bound instanceof GenericObjectType && ($boundClass === GenericObjectType::class || $bound instanceof TemplateType)) { + 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, $default); + } + + if ($bound instanceof ArrayType && ($boundClass === ArrayType::class || $bound instanceof TemplateType)) { + 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, $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, $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, $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, $default); + } + + if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) { + 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, $default); + } + + if ($bound instanceof UnionType) { + if ($boundClass === UnionType::class || $bound instanceof TemplateUnionType) { + return new TemplateUnionType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof BenevolentUnionType) { + return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound, $default); + } + } + + if ($bound instanceof IntersectionType) { + return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound, $default); } - if ($boundClass === MixedType::class) { - return new TemplateMixedType($scope, $strategy, $variance, $name); + 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); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } - public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): Type + 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 2e8e306c4c..29ecd48720 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -2,28 +2,63 @@ namespace PHPStan\Type\Generic; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\ErrorType; -use PHPStan\Type\StaticType; +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): 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): 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()) ?? $type; + $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) { - $newType = $type->getBound(); + if ($newType instanceof ErrorType && !$keepErrorTypes) { + return $traverse($type->getDefault() ?? $type->getBound()); } - if ($newType instanceof StaticType) { - $newType = $newType->getStaticObjectType(); + + $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; @@ -33,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 { @@ -45,11 +91,41 @@ public static function resolveToBounds(Type $type): Type } /** - * Switches all template types to their argument strategy + * @template T of Type + * @param T $type + * @return T */ public static function toArgument(Type $type): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + $ownedTemplates = []; + + /** @var T */ + 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()); } @@ -58,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 8ae4e0bd2d..49f3a08496 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -2,22 +2,49 @@ namespace PHPStan\Type\Generic; -use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; - -class TemplateTypeMap +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function count; + +/** + * @api + */ +final class TemplateTypeMap { private static ?TemplateTypeMap $empty = null; - /** @var array */ - private array $types; + private ?TemplateTypeMap $resolvedToBounds = null; - /** @param array $types */ - public function __construct(array $types) + /** + * @api + * @param array $types + * @param array $lowerBoundTypes + */ + public function __construct(private array $types, private array $lowerBoundTypes = []) { - $this->types = $types; + } + + public function convertToLowerBoundTypes(): self + { + $lowerBoundTypes = $this->types; + foreach ($this->lowerBoundTypes as $name => $type) { + if (isset($lowerBoundTypes[$name])) { + $intersection = TypeCombinator::intersect($lowerBoundTypes[$name], $type); + if ($intersection instanceof NeverType) { + continue; + } + $lowerBoundTypes[$name] = $intersection; + } else { + $lowerBoundTypes[$name] = $type; + } + } + + return new self([], $lowerBoundTypes); } public static function createEmpty(): self @@ -28,7 +55,7 @@ public static function createEmpty(): self return $empty; } - $empty = new self([]); + $empty = new self([], []); self::$empty = $empty; return $empty; @@ -36,23 +63,56 @@ public static function createEmpty(): self public function isEmpty(): bool { - return count($this->types) === 0; + return $this->count() === 0; } public function count(): int { - return count($this->types); + return count($this->types + $this->lowerBoundTypes); } - /** @return array */ + /** @return array */ public function getTypes(): array { - return $this->types; + $types = $this->types; + foreach ($this->lowerBoundTypes as $name => $type) { + if (array_key_exists($name, $types)) { + continue; + } + + $types[$name] = $type; + } + + return $types; + } + + public function hasType(string $name): bool + { + return array_key_exists($name, $this->getTypes()); } public function getType(string $name): ?Type { - return $this->types[$name] ?? null; + return $this->getTypes()[$name] ?? null; + } + + public function unsetType(string $name): self + { + if (!$this->hasType($name)) { + return $this; + } + + $types = $this->types; + $lowerBoundTypes = $this->lowerBoundTypes; + + unset($types[$name]); + unset($lowerBoundTypes[$name]); + + if (count($types) === 0 && count($lowerBoundTypes) === 0) { + return self::createEmpty(); + } + + return new self($types, $lowerBoundTypes); } public function union(self $other): self @@ -67,7 +127,48 @@ public function union(self $other): self } } - return new self($result); + $resultLowerBoundTypes = $this->lowerBoundTypes; + foreach ($other->lowerBoundTypes as $name => $type) { + if (isset($resultLowerBoundTypes[$name])) { + $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type); + if ($intersection instanceof NeverType) { + continue; + } + $resultLowerBoundTypes[$name] = $intersection; + } else { + $resultLowerBoundTypes[$name] = $type; + } + } + + return new self($result, $resultLowerBoundTypes); + } + + public function benevolentUnion(self $other): self + { + $result = $this->types; + + foreach ($other->types as $name => $type) { + if (isset($result[$name])) { + $result[$name] = TypeUtils::toBenevolentUnion(TypeCombinator::union($result[$name], $type)); + } else { + $result[$name] = $type; + } + } + + $resultLowerBoundTypes = $this->lowerBoundTypes; + foreach ($other->lowerBoundTypes as $name => $type) { + if (isset($resultLowerBoundTypes[$name])) { + $intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type); + if ($intersection instanceof NeverType) { + continue; + } + $resultLowerBoundTypes[$name] = $intersection; + } else { + $resultLowerBoundTypes[$name] = $type; + } + } + + return new self($result, $resultLowerBoundTypes); } public function intersect(self $other): self @@ -82,14 +183,23 @@ public function intersect(self $other): self } } - return new self($result); + $resultLowerBoundTypes = $this->lowerBoundTypes; + foreach ($other->lowerBoundTypes as $name => $type) { + if (isset($resultLowerBoundTypes[$name])) { + $resultLowerBoundTypes[$name] = TypeCombinator::union($resultLowerBoundTypes[$name], $type); + } else { + $resultLowerBoundTypes[$name] = $type; + } + } + + return new self($result, $resultLowerBoundTypes); } /** @param callable(string,Type):Type $cb */ public function map(callable $cb): self { $types = []; - foreach ($this->types as $name => $type) { + foreach ($this->getTypes() as $name => $type) { $types[$name] = $cb($name, $type); } @@ -98,24 +208,13 @@ public function map(callable $cb): self public function resolveToBounds(): self { - return $this->map(static function (string $name, Type $type): Type { - $type = TemplateTypeHelper::resolveToBounds($type); - if ($type instanceof MixedType && $type->isExplicitMixed()) { - return new MixedType(false); - } - - return $type; - }); - } - - /** - * @param mixed[] $properties - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['types'] - ); + if ($this->resolvedToBounds !== null) { + return $this->resolvedToBounds; + } + 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 89e69d6051..3e18bccf2d 100644 --- a/src/Type/Generic/TemplateTypeParameterStrategy.php +++ b/src/Type/Generic/TemplateTypeParameterStrategy.php @@ -2,21 +2,20 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; 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 CompoundTypeHelper::accepts($right, $left, $strictTypes); + return $right->isAcceptedBy($left, $strictTypes); } return $left->getBound()->accepts($right, $strictTypes); @@ -27,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 a88e5c3ce1..0be67d5e08 100644 --- a/src/Type/Generic/TemplateTypeReference.php +++ b/src/Type/Generic/TemplateTypeReference.php @@ -2,17 +2,11 @@ namespace PHPStan\Type\Generic; -class TemplateTypeReference +final class TemplateTypeReference { - private TemplateType $type; - - private TemplateTypeVariance $positionVariance; - - public function __construct(TemplateType $type, TemplateTypeVariance $positionVariance) + public function __construct(private TemplateType $type, private TemplateTypeVariance $positionVariance) { - $this->type = $type; - $this->positionVariance = $positionVariance; } public function getType(): TemplateType diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index 8e58ac9ba4..f362ecadd4 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -2,12 +2,15 @@ namespace PHPStan\Type\Generic; -class TemplateTypeScope -{ +use function sprintf; - private ?string $className; +final class TemplateTypeScope +{ - private ?string $functionName; + public static function createWithAnonymousFunction(): self + { + return new self(null, null); + } public static function createWithFunction(string $functionName): self { @@ -24,30 +27,36 @@ public static function createWithClass(string $className): self return new self($className, null); } - private function __construct(?string $className, ?string $functionName) + private function __construct(private ?string $className, private ?string $functionName) { - $this->className = $className; - $this->functionName = $functionName; } + /** @api */ public function getClassName(): ?string { return $this->className; } + /** @api */ public function getFunctionName(): ?string { return $this->functionName; } + /** @api */ public function equals(self $other): bool { return $this->className === $other->className && $this->functionName === $other->functionName; } + /** @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 new file mode 100644 index 0000000000..52c13a9680 --- /dev/null +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -0,0 +1,369 @@ +name; + } + + public function getScope(): TemplateTypeScope + { + return $this->scope; + } + + /** @return TBound */ + 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 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 { + $boundDescription = sprintf(' of %s', $this->bound->describe($level)); + } + $defaultDescription = $this->default !== null ? sprintf(' = %s', $this->default->describe($level)) : ''; + return sprintf( + '%s%s%s', + $this->name, + $boundDescription, + $defaultDescription, + ); + }; + + return $level->handle( + $basicDescription, + $basicDescription, + fn (): string => sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'), + ); + } + + public function isArgument(): bool + { + return $this->strategy->isArgument(); + } + + public function toArgument(): TemplateType + { + return new self( + $this->scope, + new TemplateTypeArgumentStrategy(), + $this->variance, + $this->name, + TemplateTypeHelper::toArgument($this->getBound()), + $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, + ); + } + + public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult + { + return $this->variance->isValidVariance($this, $a, $b); + } + + public function subtract(Type $typeToRemove): Type + { + $removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove); + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $removedBound, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + public function getTypeWithoutSubtractedType(): Type + { + $bound = $this->getBound(); + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + return $this; + } + + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound->getTypeWithoutSubtractedType(), + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + $bound = $this->getBound(); + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + return $this; + } + + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound->changeSubtractedType($subtractedType), + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + public function getSubtractedType(): ?Type + { + $bound = $this->getBound(); + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + return null; + } + + return $bound->getSubtractedType(); + } + + 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->default === null && $type->default === null) + || ($this->default !== null && $type->default !== null && $this->default->equals($type->default)) + ); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + /** @var TBound $bound */ + $bound = $this->getBound(); + if ( + !$acceptingType instanceof $bound + && !$this instanceof $acceptingType + && !$acceptingType instanceof TemplateType + && ($acceptingType instanceof UnionType || $acceptingType instanceof IntersectionType) + ) { + return $acceptingType->accepts($this, $strictTypes); + } + + if (!$acceptingType instanceof TemplateType) { + return $acceptingType->accepts($this->getBound(), $strictTypes); + } + + if ($this->getScope()->equals($acceptingType->getScope()) && $this->getName() === $acceptingType->getName()) { + return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes); + } + + return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes) + ->and(new AcceptsResult(TrinaryLogic::createMaybe(), [])); + } + + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + return $this->strategy->accepts($this, $type, $strictTypes); + } + + 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(IsSuperTypeOfResult::createMaybe()); + } + + public function isSubTypeOf(Type $type): IsSuperTypeOfResult + { + /** @var TBound $bound */ + $bound = $this->getBound(); + if ( + !$type instanceof $bound + && !$this instanceof $type + && !$type instanceof TemplateType + && ($type instanceof UnionType || $type instanceof IntersectionType) + ) { + return $type->isSuperTypeOf($this); + } + + if (!$type instanceof TemplateType) { + return $type->isSuperTypeOf($this->getBound()); + } + + if ($this->getScope()->equals($type->getScope()) && $this->getName() === $type->getName()) { + return $type->getBound()->isSuperTypeOf($this->getBound()); + } + + return $type->getBound()->isSuperTypeOf($this->getBound()) + ->and(IsSuperTypeOfResult::createMaybe()); + } + + public function toArrayKey(): Type + { + 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() + ) { + return new TemplateTypeMap([ + $this->name => $receivedType, + ]); + } + + $map = $this->getBound()->inferTemplateTypes($receivedType); + $resolvedBound = TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes( + $this->getBound(), + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + )); + if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) { + return (new TemplateTypeMap([ + $this->name => $receivedType, + ]))->union($map); + } + + return $map; + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return [new TemplateTypeReference($this, $positionVariance)]; + } + + public function getVariance(): TemplateTypeVariance + { + return $this->variance; + } + + public function getStrategy(): TemplateTypeStrategy + { + return $this->strategy; + } + + protected function shouldGeneralizeInferredType(): bool + { + return true; + } + + public function traverse(callable $cb): Type + { + $bound = $cb($this->getBound()); + $default = $this->getDefault() !== null ? $cb($this->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 traverseSimultaneously(Type $right, callable $cb): Type + { + 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 701791b6a1..a630895bed 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,30 +2,38 @@ 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; -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; - private int $value; - - private function __construct(int $value) + private function __construct(private int $value) { - $this->value = $value; } private static function create(int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -49,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; @@ -69,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()) { @@ -78,45 +96,94 @@ 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): bool + public function isValidVariance(TemplateType $templateType, Type $a, Type $b): IsSuperTypeOfResult { + if ($b instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); + } + if ($a instanceof MixedType && !$a instanceof TemplateType) { - return true; + return IsSuperTypeOfResult::createYes(); + } + + if ($a instanceof BenevolentUnionType) { + if (!$a->isSuperTypeOf($b)->no()) { + return IsSuperTypeOfResult::createYes(); + } + } + + if ($b instanceof BenevolentUnionType) { + if (!$b->isSuperTypeOf($a)->no()) { + return IsSuperTypeOfResult::createYes(); + } } if ($b instanceof MixedType && !$b instanceof TemplateType) { - return true; + return IsSuperTypeOfResult::createYes(); } if ($this->invariant()) { - return $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()) { - return $a->isSuperTypeOf($b)->yes(); + return $a->isSuperTypeOf($b); } if ($this->contravariant()) { - return $b->isSuperTypeOf($a)->yes(); + return $b->isSuperTypeOf($a); } - throw new \PHPStan\ShouldNotHappenException(); + if ($this->bivariant()) { + return IsSuperTypeOfResult::createYes(); + } + + throw new ShouldNotHappenException(); } public function equals(self $other): bool @@ -128,6 +195,7 @@ public function validPosition(self $other): bool { return $other->value === $this->value || $other->invariant() + || $this->bivariant() || $this->static(); } @@ -142,18 +210,30 @@ public function describe(): string return 'contravariant'; case self::STATIC: return 'static'; + case self::BIVARIANT: + return 'bivariant'; } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** - * @param array{value: int} $properties - * @return self + * @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 new file mode 100644 index 0000000000..dc58af565a --- /dev/null +++ b/src/Type/Generic/TemplateUnionType.php @@ -0,0 +1,54 @@ + */ + 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()); + + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + 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 1643770ae8..0000000000 --- a/src/Type/GenericTypeVariableResolver.php +++ /dev/null @@ -1,29 +0,0 @@ -getAncestorWithClassName($genericClassName); - if ($ancestor === null) { - return null; - } - - $classReflection = $ancestor->getClassReflection(); - if ($classReflection === null) { - return null; - } - - $templateTypeMap = $classReflection->getActiveTemplateTypeMap(); - - return $templateTypeMap->getType($typeVariableName); - } - -} 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 8a2a6a51b9..1c956015dd 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -2,164 +2,756 @@ 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; - -class IntegerRangeType extends IntegerType +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; +use function sprintf; +use const PHP_INT_MAX; +use const PHP_INT_MIN; + +/** @api */ +class IntegerRangeType extends IntegerType implements CompoundType { - private int $min; - - private int $max; - - private function __construct(?int $min, ?int $max) + private function __construct(private ?int $min, private ?int $max) { + parent::__construct(); assert($min === null || $max === null || $min <= $max); + assert($min !== null || $max !== null); + } + + public static function fromInterval(?int $min, ?int $max, int $shift = 0): Type + { + if ($min !== null && $max !== null) { + if ($min > $max) { + return new NeverType(); + } + if ($min === $max) { + return new ConstantIntegerType($min + $shift); + } + } + + if ($min === null && $max === null) { + return new IntegerType(); + } - $this->min = $min ?? PHP_INT_MIN; - $this->max = $max ?? PHP_INT_MAX; + return (new self($min, $max))->shift($shift); } + protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $maxB, bool $touchingIsDisjoint = true): bool + { + $offset = $touchingIsDisjoint ? 0 : 1; + return $minA !== null && $maxB !== null && $minA > $maxB + $offset + || $maxA !== null && $minB !== null && $maxA + $offset < $minB; + } - public static function fromInterval(?int $min, ?int $max): Type + /** + * Return the range of integers smaller than the given value + * + * @param int|float $value + */ + public static function createAllSmallerThan($value): Type { - $min = $min ?? PHP_INT_MIN; - $max = $max ?? PHP_INT_MAX; + if (is_int($value)) { + return self::fromInterval(null, $value, -1); + } + + if ($value > PHP_INT_MAX) { + return new IntegerType(); + } - if ($min > $max) { + if ($value <= PHP_INT_MIN) { return new NeverType(); } - if ($min === $max) { - return new ConstantIntegerType($min); + return self::fromInterval(null, (int) ceil($value), -1); + } + + /** + * Return the range of integers smaller than or equal to the given value + * + * @param int|float $value + */ + public static function createAllSmallerThanOrEqualTo($value): Type + { + if (is_int($value)) { + return self::fromInterval(null, $value); } - if ($min === PHP_INT_MIN && $max === PHP_INT_MAX) { + if ($value >= PHP_INT_MAX) { return new IntegerType(); } - return new self($min, $max); + if ($value < PHP_INT_MIN) { + return new NeverType(); + } + + return self::fromInterval(null, (int) floor($value)); } + /** + * Return the range of integers greater than the given value + * + * @param int|float $value + */ + public static function createAllGreaterThan($value): Type + { + if (is_int($value)) { + return self::fromInterval($value, null, 1); + } - public function getMin(): int + if ($value < PHP_INT_MIN) { + return new IntegerType(); + } + + if ($value >= PHP_INT_MAX) { + return new NeverType(); + } + + return self::fromInterval((int) floor($value), null, 1); + } + + /** + * Return the range of integers greater than or equal to the given value + * + * @param int|float $value + */ + public static function createAllGreaterThanOrEqualTo($value): Type { - return $this->min; + if (is_int($value)) { + return self::fromInterval($value, null); + } + + if ($value <= PHP_INT_MIN) { + return new IntegerType(); + } + + if ($value > PHP_INT_MAX) { + return new NeverType(); + } + + return self::fromInterval((int) ceil($value), null); } + public function getMin(): ?int + { + return $this->min; + } - public function getMax(): int + public function getMax(): ?int { return $this->max; } - public function describe(VerbosityLevel $level): string { - if ($this->min === PHP_INT_MIN) { - return sprintf('int', $this->max); + return sprintf('int<%s, %s>', $this->min ?? 'min', $this->max ?? 'max'); + } + + public function shift(int $amount): Type + { + if ($amount === 0) { + return $this; } - if ($this->max === PHP_INT_MAX) { - return sprintf('int<%d, max>', $this->min); + $min = $this->min; + $max = $this->max; + + if ($amount < 0) { + if ($max !== null) { + if ($max < PHP_INT_MIN - $amount) { + return new NeverType(); + } + $max += $amount; + } + if ($min !== null) { + $min = $min < PHP_INT_MIN - $amount ? null : $min + $amount; + } + } else { + if ($min !== null) { + if ($min > PHP_INT_MAX - $amount) { + return new NeverType(); + } + $min += $amount; + } + if ($max !== null) { + $max = $max > PHP_INT_MAX - $amount ? null : $max + $amount; + } } - return sprintf('int<%d, %d>', $this->min, $this->max); + return self::fromInterval($min, $max); } + public function accepts(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof parent) { + return $this->isSuperTypeOf($type)->toAcceptsResult(); + } + + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + return AcceptsResult::createNo(); + } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof self) { - if ($this->min > $type->max || $this->max < $type->min) { - return TrinaryLogic::createNo(); + if ($type instanceof self || $type instanceof ConstantIntegerType) { + if ($type instanceof self) { + $typeMin = $type->min; + $typeMax = $type->max; + } else { + $typeMin = $type->getValue(); + $typeMax = $type->getValue(); } - if ($this->min <= $type->min && $this->max >= $type->max) { - return TrinaryLogic::createYes(); + if (self::isDisjoint($this->min, $this->max, $typeMin, $typeMax)) { + return IsSuperTypeOfResult::createNo(); } - return TrinaryLogic::createMaybe(); - } - - if ($type instanceof ConstantIntegerType) { - if ($this->min <= $type->getValue() && $type->getValue() <= $this->max) { - return TrinaryLogic::createYes(); + if ( + ($this->min === null || $typeMin !== null && $this->min <= $typeMin) + && ($this->max === null || $typeMax !== null && $this->max >= $typeMax) + ) { + return IsSuperTypeOfResult::createYes(); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return IsSuperTypeOfResult::createMaybe(); } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return IsSuperTypeOfResult::createNo(); } + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof parent) { + return $otherType->isSuperTypeOf($this); + } + + if ($otherType instanceof UnionType) { + return $this->isSubTypeOfUnionWithReason($otherType); + } + + if ($otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return IsSuperTypeOfResult::createNo(); + } - public function isSuperTypeOf(Type $type): TrinaryLogic + private function isSubTypeOfUnionWithReason(UnionType $otherType): IsSuperTypeOfResult { - if ($type instanceof self) { - if ($this->min > $type->max || $this->max < $type->min) { - return TrinaryLogic::createNo(); + 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(); } + } - if ($this->min <= $type->min && $this->max >= $type->max) { - return TrinaryLogic::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 + { + return $type instanceof self && $this->min === $type->min && $this->max === $type->max; + } - return TrinaryLogic::createMaybe(); + public function generalize(GeneralizePrecision $precision): Type + { + return new IntegerType(); + } + + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + if ($this->min === null) { + $minIsSmaller = TrinaryLogic::createYes(); + } else { + $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThan($otherType, $phpVersion); } - if ($type instanceof ConstantIntegerType) { - if ($this->min <= $type->getValue() && $type->getValue() <= $this->max) { - return TrinaryLogic::createYes(); - } + if ($this->max === null) { + $maxIsSmaller = TrinaryLogic::createNo(); + } else { + $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion); + } - return TrinaryLogic::createNo(); + // 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, + ); } - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); + } + + public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + if ($this->min === null) { + $minIsSmaller = TrinaryLogic::createYes(); + } else { + $minIsSmaller = (new ConstantIntegerType($this->min))->isSmallerThanOrEqual($otherType, $phpVersion); } - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); + if ($this->max === null) { + $maxIsSmaller = TrinaryLogic::createNo(); + } else { + $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::createNo(); + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + if ($this->min === null) { + $minIsSmaller = TrinaryLogic::createNo(); + } else { + $minIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->min)), $phpVersion); + } - public function equals(Type $type): bool + if ($this->max === null) { + $maxIsSmaller = TrinaryLogic::createYes(); + } else { + $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, PhpVersion $phpVersion): TrinaryLogic { - return $type instanceof self && $this->min === $type->min && $this->max === $type->max; + if ($this->min === null) { + $minIsSmaller = TrinaryLogic::createNo(); + } else { + $minIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->min)), $phpVersion); + } + + if ($this->max === null) { + $maxIsSmaller = TrinaryLogic::createYes(); + } else { + $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(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new ConstantBooleanType(true), + ]; + + if ($this->max !== null) { + $subtractedTypes[] = self::createAllGreaterThanOrEqualTo($this->max); + } - public function generalize(): Type + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + $subtractedTypes = []; + + if ($this->max !== null) { + $subtractedTypes[] = self::createAllGreaterThan($this->max); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + new NullType(), + new ConstantBooleanType(false), + ]; + + if ($this->min !== null) { + $subtractedTypes[] = self::createAllSmallerThanOrEqualTo($this->min); + } + + if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) { + $subtractedTypes[] = new ConstantBooleanType(true); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { - return new parent(); + $subtractedTypes = []; + + if ($this->min !== null) { + $subtractedTypes[] = self::createAllSmallerThan($this->min); + } + + if ($this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0) { + $subtractedTypes[] = new NullType(); + $subtractedTypes[] = new ConstantBooleanType(false); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function toNumber(): Type + public function toBoolean(): BooleanType { - return new parent(); + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); + if ($isZero->no()) { + return new ConstantBooleanType(true); + } + + if ($isZero->maybe()) { + return new 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(), + ]); + } /** - * @param mixed[] $properties - * @return Type + * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType + * */ - public static function __set_state(array $properties): Type + public function tryUnion(Type $otherType): ?Type { - return new self($properties['min'], $properties['max']); + if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) { + if ($otherType instanceof self) { + $otherMin = $otherType->min; + $otherMax = $otherType->max; + } else { + $otherMin = $otherType->getValue(); + $otherMax = $otherType->getValue(); + } + + if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) { + return null; + } + + return self::fromInterval( + $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null, + $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null, + ); + } + + if (get_class($otherType) === parent::class) { + return $otherType; + } + + return null; + } + + /** + * Return the intersection with another type, but only if it can be expressed in a simpler way than using + * IntersectionType + * + */ + public function tryIntersect(Type $otherType): ?Type + { + if ($otherType instanceof self || $otherType instanceof ConstantIntegerType) { + if ($otherType instanceof self) { + $otherMin = $otherType->min; + $otherMax = $otherType->max; + } else { + $otherMin = $otherType->getValue(); + $otherMax = $otherType->getValue(); + } + + if (self::isDisjoint($this->min, $this->max, $otherMin, $otherMax, false)) { + return new NeverType(); + } + + if ($this->min === null) { + $newMin = $otherMin; + } elseif ($otherMin === null) { + $newMin = $this->min; + } else { + $newMin = max($this->min, $otherMin); + } + + if ($this->max === null) { + $newMax = $otherMax; + } elseif ($otherMax === null) { + $newMax = $this->max; + } else { + $newMax = min($this->max, $otherMax); + } + + return self::fromInterval($newMin, $newMax); + } + + if (get_class($otherType) === parent::class) { + return $this; + } + + return null; + } + + /** + * Return the different with another type, or null if it cannot be represented. + * + */ + public function tryRemove(Type $typeToRemove): ?Type + { + if (get_class($typeToRemove) === parent::class) { + return new NeverType(); + } + + if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) { + if ($typeToRemove instanceof self) { + $removeMin = $typeToRemove->min; + $removeMax = $typeToRemove->max; + } else { + $removeMin = $typeToRemove->getValue(); + $removeMax = $typeToRemove->getValue(); + } + + if ( + $this->min !== null && $removeMax !== null && $removeMax < $this->min + || $this->max !== null && $removeMin !== null && $this->max < $removeMin + ) { + return $this; + } + + if ($removeMin !== null && $removeMin !== PHP_INT_MIN) { + $lowerPart = self::fromInterval($this->min, $removeMin - 1); + } else { + $lowerPart = null; + } + if ($removeMax !== null && $removeMax !== PHP_INT_MAX) { + $upperPart = self::fromInterval($removeMax + 1, $this->max); + } else { + $upperPart = null; + } + + if ($lowerPart !== null && $upperPart !== null) { + return TypeCombinator::union($lowerPart, $upperPart); + } + + return $lowerPart ?? $upperPart; + } + + 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); + } + + /** + * @return list + */ + 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 + { + $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 4b16c9b781..fcb6fcd893 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -2,37 +2,54 @@ 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; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +/** @api */ class IntegerType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; + use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + public function __construct() + { + } public function describe(VerbosityLevel $level): string { return 'int'; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function getConstantStrings(): array { - return new self(); + return []; } public function toNumber(): Type @@ -40,6 +57,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } + public function toFloat(): Type { return new FloatType(); @@ -52,7 +74,12 @@ public function toInteger(): Type public function toString(): Type { - return new StringType(); + return new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), + new AccessoryNumericStringType(), + ]); } public function toArray(): Type @@ -60,28 +87,117 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + [], + TrinaryLogic::createYes(), ); } - public function isOffsetAccessible(): TrinaryLogic + 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 hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isBoolean(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type + 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) { + if ($typeToRemove instanceof IntegerRangeType) { + $removeValueMin = $typeToRemove->getMin(); + $removeValueMax = $typeToRemove->getMax(); + } else { + $removeValueMin = $typeToRemove->getValue(); + $removeValueMax = $typeToRemove->getValue(); + } + $lowerPart = $removeValueMin !== null ? IntegerRangeType::fromInterval(null, $removeValueMin, -1) : null; + $upperPart = $removeValueMax !== null ? IntegerRangeType::fromInterval($removeValueMax, null, +1) : null; + if ($lowerPart !== null && $upperPart !== null) { + return new UnionType([$lowerPart, $upperPart]); + } + return $lowerPart ?? $upperPart ?? new NeverType(); + } + + return null; + } + + public function getFiniteTypes(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type { - return new ErrorType(); + return ExponentiateHelper::exponentiate($this, $exponent); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function toPhpDocNode(): TypeNode { - return new ErrorType(); + return new IdentifierTypeNode('int'); } } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 851fb49a8b..bfe0e31471 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2,29 +2,82 @@ 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\PropertyReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\TrivialParametersAcceptor; -use PHPStan\Reflection\Type\IntersectionTypeMethodReflection; +use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\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 { - /** @var \PHPStan\Type\Type[] */ - private array $types; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; + + private bool $sortedTypes = false; /** + * @api * @param Type[] $types */ - public function __construct(array $types) + public function __construct(private array $types) { - $this->types = UnionTypeHelper::sortTypes($types); + if (count($types) < 2) { + throw new ShouldNotHappenException(sprintf( + 'Cannot create %s with: %s', + self::class, + implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)), + )); + } } /** @@ -36,65 +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 accepts(Type $otherType, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array { + $objectClassNames = []; foreach ($this->types as $type) { - if (!$type->accepts($otherType, $strictTypes)->yes()) { - return TrinaryLogic::createNo(); + $innerObjectClassNames = $type->getObjectClassNames(); + foreach ($innerObjectClassNames as $innerObjectClassName) { + $objectClassNames[] = $innerObjectClassName; } } - return TrinaryLogic::createYes(); + return array_values(array_unique($objectClassNames)); } - public function isSuperTypeOf(Type $otherType): TrinaryLogic + public function getObjectClassReflections(): array { - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->isSuperTypeOf($otherType); + $reflections = []; + foreach ($this->types as $type) { + foreach ($type->getObjectClassReflections() as $reflection) { + $reflections[] = $reflection; + } } - return TrinaryLogic::createYes()->and(...$results); + return $reflections; } - public function isSubTypeOf(Type $otherType): TrinaryLogic + public function getArrays(): array { - if ($otherType instanceof self || $otherType instanceof UnionType) { - return $otherType->isSuperTypeOf($this); + $arrays = []; + foreach ($this->types as $type) { + foreach ($type->getArrays() as $array) { + $arrays[] = $array; + } } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $otherType->isSuperTypeOf($innerType); + return $arrays; + } + + public function getConstantArrays(): array + { + $constantArrays = []; + foreach ($this->types as $type) { + foreach ($type->getConstantArrays() as $constantArray) { + $constantArrays[] = $constantArray; + } } - return TrinaryLogic::maxMin(...$results); + return $constantArrays; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function getConstantStrings(): array { - if ($acceptingType instanceof self || $acceptingType instanceof UnionType) { - return $acceptingType->isSuperTypeOf($this); + $strings = []; + foreach ($this->types as $type) { + foreach ($type->getConstantStrings() as $string) { + $strings[] = $string; + } } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); + return $strings; + } + + public function accepts(Type $otherType, bool $strictTypes): AcceptsResult + { + $result = AcceptsResult::createYes(); + foreach ($this->types as $type) { + $result = $result->and($type->accepts($otherType, $strictTypes)); } - return TrinaryLogic::maxMin(...$results); + if (!$result->yes()) { + $isList = $otherType->isList(); + $reasons = $result->reasons; + $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType); + if ($this->isList()->yes() && !$isList->yes()) { + $reasons[] = sprintf( + '%s %s a list.', + $otherType->describe($verbosity), + $isList->no() ? 'is not' : 'might not be', + ); + } + + $isNonEmpty = $otherType->isIterableAtLeastOnce(); + if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) { + $reasons[] = sprintf( + '%s %s empty.', + $otherType->describe($verbosity), + $isNonEmpty->no() ? 'is' : 'might be', + ); + } + + if (count($reasons) > 0) { + return new AcceptsResult($result->result, $reasons); + } + } + + return $result; + } + + public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof IntersectionType && $this->equals($otherType)) { + return IsSuperTypeOfResult::createYes(); + } + + if ($otherType instanceof NeverType) { + return IsSuperTypeOfResult::createYes(); + } + + return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) { + return $otherType->isSuperTypeOf($this); + } + + $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 $result; + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + $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 $result; } public function equals(Type $type): bool { - if (!$type instanceof self) { + if (!$type instanceof static) { return false; } @@ -102,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 @@ -116,114 +300,288 @@ 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[] = TypeUtils::generalizeType($type)->describe($level); + $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level); } - return implode('&', $typeNames); - }, - function () use ($level): string { - $typeNames = []; - foreach ($this->types as $type) { - if ($type instanceof AccessoryType) { - continue; + 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[] = $type->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); }, - function () use ($level): string { - $typeNames = []; - foreach ($this->types as $type) { - $typeNames[] = $type->describe($level); + fn (): string => $this->describeItself($level, true), + fn (): string => $this->describeItself($level, false), + ); + } + + private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string + { + $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 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]); + } } - return implode('&', $typeNames); + $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(); + $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 ($type instanceof CallableType && $type->isCommonCallable()) { + $typesToDescribe[$i] = $type; + $skipTypeNames[] = 'object'; + $skipTypeNames[] = 'string'; + continue; + } + + if (!$type instanceof AccessoryType) { + $baseTypes[$i] = $type; + continue; + } + + if ($skipAccessoryTypes) { + continue; + } + + $typesToDescribe[$i] = $type; + } + + foreach ($baseTypes as $i => $type) { + $typeDescription = $type->describe($level); + + if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) { + foreach ($typesToDescribe as $j => $typeToDescribe) { + if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) { + $describedTypes[$i] = 'callable-' . $typeDescription; + unset($typesToDescribe[$j]); + continue 2; + } + } + } + + if (in_array($typeDescription, $skipTypeNames, true)) { + continue; + } + + $describedTypes[$i] = $type->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 function (Type $type): TrinaryLogic { - return $type->canAccessProperties(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); } public function hasProperty(string $propertyName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($propertyName): TrinaryLogic { - return $type->hasProperty($propertyName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; foreach ($this->types as $type) { - if ($type->hasProperty($propertyName)->yes()) { - return $type->getProperty($propertyName, $scope); + if (!$type->hasProperty($propertyName)->yes()) { + continue; } + + $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new ShouldNotHappenException(); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; } - throw new \PHPStan\ShouldNotHappenException(); + return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); } public function canCallMethods(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canCallMethods(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); } public function hasMethod(string $methodName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($methodName): TrinaryLogic { - return $type->hasMethod($methodName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); + } + + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection + { + return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - $methods = []; + $methodPrototypes = []; foreach ($this->types as $type) { if (!$type->hasMethod($methodName)->yes()) { continue; } - $methods[] = $type->getMethod($methodName, $scope); + $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this); } - $methodsCount = count($methods); + $methodsCount = count($methodPrototypes); if ($methodsCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($methodsCount === 1) { - return $methods[0]; + return $methodPrototypes[0]; } - return new IntersectionTypeMethodReflection($methodName, $methods); + return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes); } public function canAccessConstants(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); } public function hasConstant(string $constantName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName)); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { foreach ($this->types as $type) { if ($type->hasConstant($constantName)->yes()) { @@ -231,87 +589,350 @@ public function getConstant(string $constantName): ConstantReflection } } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isIterable(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isIterable(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable()); } public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isIterableAtLeastOnce(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); + } + + public function getArraySize(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize()); } public function getIterableKeyType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getIterableKeyType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); + } + + public function 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 function (Type $type): Type { - return $type->getIterableValueType(); - }); + 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 function (Type $type): TrinaryLogic { - return $type->isArray(); - }); + 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()); + } + + public function isNumericString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); + } + + public function 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 function (Type $type): TrinaryLogic { - return $type->isOffsetAccessible(); - }); + 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 { - return $this->intersectResults(static function (Type $type) use ($offsetType): TrinaryLogic { - return $type->hasOffsetValueType($offsetType); - }); + 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 function (Type $type) use ($offsetType): Type { - return $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 + { + 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 + { + 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 setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function getValuesArray(): Type { - return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType): Type { - return $type->setOffsetValueType($offsetType, $valueType); - }); + 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 function (Type $type): TrinaryLogic { - return $type->isCallable(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return [new TrivialParametersAcceptor()]; @@ -319,16 +940,116 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) public function isCloneable(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isCloneable(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); + } + + public function isSmallerThan(Type $otherType, 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->isFalse()); + } + + public function isBoolean(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean()); + } + + public function isFloat(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat()); + } + + public function isInteger(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); + } + + public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion)); + } + + public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion)); + } + + public function getSmallerType(PhpVersion $phpVersion): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion)); + } + + public function getSmallerOrEqualType(PhpVersion $phpVersion): Type + { + 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 { - $type = $this->intersectTypes(static function (Type $type): BooleanType { - return $type->toBoolean(); - }); + $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean()); if (!$type instanceof BooleanType) { return new BooleanType(); @@ -339,66 +1060,73 @@ public function toBoolean(): BooleanType public function toNumber(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toNumber(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber()); + + return $type; + } + + public function toAbsoluteNumber(): Type + { + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); return $type; } public function toString(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toString(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString()); return $type; } public function toInteger(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toInteger(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger()); return $type; } public function toFloat(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toFloat(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat()); return $type; } public function toArray(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toArray(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray()); return $type; } - public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + public function toArrayKey(): Type { - $types = TemplateTypeMap::createEmpty(); + if ($this->isNumericString()->yes()) { + return TypeCombinator::union( + new IntegerType(), + $this, + ); + } - 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; @@ -437,28 +1165,75 @@ public function traverse(callable $cb): Type return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof self) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeCombinator::intersect(...$types); + } + + return $this; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); + } + + public function exponentiate(Type $exponent): Type { - return 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; } /** * @param callable(Type $type): TrinaryLogic $getResult - * @return TrinaryLogic */ private function intersectResults(callable $getResult): TrinaryLogic { - $operands = array_map($getResult, $this->types); - return TrinaryLogic::maxMin(...$operands); + return TrinaryLogic::lazyMaxMin($this->types, $getResult); } /** * @param callable(Type $type): Type $getType - * @return Type */ private function intersectTypes(callable $getType): Type { @@ -466,4 +1241,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 0551643004..2e6d26a381 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -2,36 +2,49 @@ 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; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use Traversable; +use function array_merge; +use function sprintf; +/** @api */ class IterableType implements CompoundType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; use UndecidedBooleanTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonGeneralizableTypeTrait; - private \PHPStan\Type\Type $keyType; - - private \PHPStan\Type\Type $itemType; - + /** @api */ public function __construct( - Type $keyType, - Type $itemType + private Type $keyType, + private Type $itemType, ) { - $this->keyType = $keyType; - $this->itemType = $itemType; + } + + public function getKeyType(): Type + { + return $this->keyType; } public function getItemType(): Type @@ -39,21 +52,33 @@ public function getItemType(): Type return $this->itemType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return array_merge( $this->keyType->getReferencedClasses(), - $this->getItemType()->getReferencedClasses() + $this->getItemType()->getReferencedClasses(), ); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array { - if ($type instanceof ConstantArrayType && $type->isEmpty()) { - return TrinaryLogic::createYes(); + 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) @@ -61,27 +86,31 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + 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); @@ -93,47 +122,47 @@ 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([ new ArrayType($this->keyType, $this->itemType), new IntersectionType([ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), $this, ]), ])); } 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) + $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 @@ -148,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'; @@ -176,6 +204,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -196,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(); @@ -206,19 +270,159 @@ 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 + { + 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 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 @@ -239,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), ); } @@ -257,13 +463,71 @@ public function traverse(callable $cb): Type return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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 { - return new self($properties['keyType'], $properties['itemType']); + $arrayType = new ArrayType(new MixedType(), new MixedType()); + if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { + return new GenericObjectType(Traversable::class, [ + $this->getIterableKeyType(), + $this->getIterableValueType(), + ]); + } + + $traversableType = new ObjectType(Traversable::class); + if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { + return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType()); + } + + return null; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + + if ($isMixedKeyType) { + if ($isMixedItemType) { + return new IdentifierTypeNode('iterable'); + } + + return new GenericTypeNode( + new IdentifierTypeNode('iterable'), + [ + $this->itemType->toPhpDocNode(), + ], + ); + } + + return new GenericTypeNode( + new IdentifierTypeNode('iterable'), + [ + $this->keyType->toPhpDocNode(), + $this->itemType->toPhpDocNode(), + ], + ); } } diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index f779cf4c87..5435c540ff 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -3,47 +3,55 @@ namespace PHPStan\Type; use PHPStan\TrinaryLogic; +use function get_class; 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 CompoundTypeHelper::accepts($type, $this, $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 { - return get_class($type) === self::class; + return get_class($type) === static::class; } public function traverse(callable $cb): Type @@ -51,7 +59,112 @@ 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(); + } + + 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(); } 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 @@ +container->getByType(TypeAliasResolver::class); + } + +} diff --git a/src/Type/LooseComparisonHelper.php b/src/Type/LooseComparisonHelper.php new file mode 100644 index 0000000000..b4df432c6f --- /dev/null +++ b/src/Type/LooseComparisonHelper.php @@ -0,0 +1,50 @@ +castsNumbersToStringsOnLooseComparison()) { + $isNumber = new UnionType([ + new IntegerType(), + new FloatType(), + ]); + + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $isNumber->isSuperTypeOf($rightType)->yes()) { + $stringValue = (string) $rightType->getValue(); + return new ConstantBooleanType($stringValue === $leftType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $isNumber->isSuperTypeOf($leftType)->yes()) { + $stringValue = (string) $leftType->getValue(); + return new ConstantBooleanType($stringValue === $rightType->getValue()); + } + } else { + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $rightType->isFloat()->yes()) { + $numericPart = (float) $leftType->getValue(); + return new ConstantBooleanType($numericPart === $rightType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $leftType->isFloat()->yes()) { + $numericPart = (float) $rightType->getValue(); + return new ConstantBooleanType($numericPart === $leftType->getValue()); + } + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $rightType->isInteger()->yes()) { + $numericPart = (int) $leftType->getValue(); + return new ConstantBooleanType($numericPart === $rightType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $leftType->isInteger()->yes()) { + $numericPart = (int) $rightType->getValue(); + return new ConstantBooleanType($numericPart === $leftType->getValue()); + } + } + + // @phpstan-ignore equal.notAllowed + return new ConstantBooleanType($leftType->getValue() == $rightType->getValue()); // phpcs:ignore + } + +} diff --git a/src/Type/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 @@ +isExplicitMixed = $isExplicitMixed; + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + $this->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 getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; } - public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + 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 $result; + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + 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 $this->subtractedType->isSuperTypeOf($type)->negate(); + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function searchArray(Type $needleType): Type { - return new MixedType(); + 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(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ + public function getEnumCases(): array + { + return []; + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; @@ -152,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 @@ -187,9 +389,20 @@ 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 { - return new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function canCallMethods(): TrinaryLogic @@ -202,9 +415,20 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - return new DummyMethodReflection($methodName); + $method = new DummyMethodReflection($methodName); + return new CallbackUnresolvedMethodPrototypeReflection( + $method, + $method->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function canAccessConstants(): TrinaryLogic @@ -217,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 @@ -230,16 +454,14 @@ public function isCloneable(): TrinaryLogic public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'mixed'; - }, - static function (): string { - return 'mixed'; - }, + static fn (): string => 'mixed', + static fn (): string => 'mixed', function () use ($level): string { $description = 'mixed'; if ($this->subtractedType !== null) { - $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; @@ -247,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) { @@ -257,20 +481,54 @@ function () use ($level): string { } return $description; - } + }, ); } + public function toBoolean(): BooleanType + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(StaticTypeFactory::falsey())->yes()) { + return new ConstantBooleanType(true); + } + } + + return new 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(); } @@ -281,12 +539,145 @@ 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(); } public function toArray(): Type { - return new ArrayType(new MixedType(), new MixedType()); + $mixed = new self($this->isExplicitMixed); + + 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 $this->isIterable(); + } + + public function getArraySize(): Type + { + if ($this->isIterable()->no()) { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval(0, null); + } + + public function getIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getFirstIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getLastIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getFirstIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getLastIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + + public function isOffsetAccessible(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $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(); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return new self($this->isExplicitMixed); } public function isExplicitMixed(): bool @@ -326,21 +717,339 @@ 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(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + public function isConstantArray(): TrinaryLogic { - return new self( - $properties['isExplicitMixed'], - $properties['subtractedType'] ?? null - ); + 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()) { + return $this->subtract($typeToRemove); + } + + return null; + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('mixed'); } } diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 970db59717..518ffa8f4a 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -2,26 +2,36 @@ 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\TrivialParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; -use PHPStan\Type\Traits\FalseyBooleanTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; +use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +/** @api */ class NeverType implements CompoundType { - use FalseyBooleanTypeTrait; + use UndecidedBooleanTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - private bool $isExplicit; - - public function __construct(bool $isExplicit = false) + /** @api */ + public function __construct(private bool $isExplicit = false) { - $this->isExplicit = $isExplicit; } public function isExplicit(): bool @@ -29,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 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): TrinaryLogic + 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 @@ -56,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 @@ -71,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(); @@ -81,9 +128,14 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -96,9 +148,14 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -111,9 +168,9 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isIterable(): TrinaryLogic @@ -126,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(); @@ -151,23 +258,89 @@ public function getOffsetValueType(Type $offsetType): Type return new NeverType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return new ErrorType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type + { + return new NeverType(); + } + + public function getKeysArray(): Type + { + return new NeverType(); + } + + public function getValuesArray(): Type + { + return new NeverType(); + } + + public function 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(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - return [new TrivialParametersAcceptor()]; + throw new ShouldNotHappenException(); } public function isCloneable(): TrinaryLogic @@ -180,6 +353,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toString(): Type { return $this; @@ -200,23 +378,159 @@ 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(); + } + + 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 isUppercaseString(): TrinaryLogic { return TrinaryLogic::createNo(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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 @@ -73,6 +100,32 @@ public function equals(Type $type): bool return $type instanceof self; } + 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, $phpVersion); + } + + return TrinaryLogic::createMaybe(); + } + + 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, $phpVersion); + } + + return TrinaryLogic::createMaybe(); + } + public function describe(VerbosityLevel $level): string { return 'null'; @@ -83,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(''); @@ -103,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(); @@ -118,10 +191,20 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { $array = new ConstantArrayType([], []); - return $array->setOffsetValueType($offsetType, $valueType); + return $array->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return $this; } public function traverse(callable $cb): Type @@ -129,18 +212,193 @@ 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(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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 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(PhpVersion $phpVersion): Type + { + // All falsey types except '0' + return new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantArrayType([], []), + ]); + } + + public function getGreaterType(PhpVersion $phpVersion): Type + { + // All truthy types, but also '0' + return new MixedType(false, new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantArrayType([], []), + ])); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + return new MixedType(); + } + + public function getFiniteTypes(): array + { + return [$this]; + } + + public function exponentiate(Type $exponent): Type + { + return new UnionType( + [ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], + ); + } + + public function toPhpDocNode(): TypeNode { - return new 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 f361e80101..cc834e2950 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -2,51 +2,133 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use ArrayAccess; +use ArrayObject; +use Closure; +use Countable; +use DateTimeInterface; +use Iterator; +use IteratorAggregate; +use PHPStan\Analyser\OutOfClassScope; +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\ObjectTypeMethodReflection; -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\TruthyBooleanTypeTrait; - +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use Stringable; +use Throwable; +use Traversable; +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; + +/** @api */ class ObjectType implements TypeWithClassName, SubtractableType { - use TruthyBooleanTypeTrait; + use MaybeIterableTypeTrait; + use NonArrayTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; + + 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> */ + private static array $superTypes = []; - private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; + private ?self $cachedParent = null; - private string $className; + /** @var self[]|null */ + private ?array $cachedInterfaces = null; - private ?\PHPStan\Type\Type $subtractedType; + /** @var array>> */ + private static array $methods = []; - private ?ClassReflection $classReflection; + /** @var array>> */ + private static array $properties = []; - private ?GenericObjectType $genericObjectType = null; + /** @var array> */ + private static array $ancestors = []; - /** @var array */ - private array $superTypes = []; + /** @var array */ + private array $currentAncestors = []; + private ?string $cachedDescription = null; + + /** @var array> */ + private static array $enumCases = []; + + /** @api */ public function __construct( - string $className, + private string $className, ?Type $subtractedType = null, - ?ClassReflection $classReflection = null + private ?ClassReflection $classReflection = null, ) { - $this->className = $className; + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + $this->subtractedType = $subtractedType; - $this->classReflection = $classReflection; + } + + public static function resetCaches(): void + { + self::$superTypes = []; + self::$methods = []; + self::$properties = []; + self::$ancestors = []; + self::$enumCases = []; } private static function createFromReflection(ClassReflection $reflection): self @@ -57,7 +139,10 @@ private static function createFromReflection(ClassReflection $reflection): self return new GenericObjectType( $reflection->getName(), - $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()) + $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), + null, + null, + $reflection->varianceMapToList($reflection->getCallSiteVarianceMap()), ); } @@ -77,89 +162,206 @@ 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 { - $classReflection = $this->getClassReflection(); + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!$scope->isInClass()) { + $canAccessProperty = 'no'; + } else { + $canAccessProperty = $scope->getClassReflection()->getName(); + } + $description = $this->describeCache(); + + if (isset(self::$properties[$description][$propertyName][$canAccessProperty])) { + return self::$properties[$description][$propertyName][$canAccessProperty]; + } + + $nakedClassReflection = $this->getNakedClassReflection(); + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + if ($nakedClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $nakedClassReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getUnresolvedPropertyPrototype($propertyName, $scope); + } + + if (count($properties) > 0) { + if (count($properties) === 1) { + return $properties[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $properties); + } + } + } + + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { + $nakedClassReflection = $this->getClassReflection(); + } + + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + $property = $nakedClassReflection->getProperty($propertyName, $scope); + + $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); + $resolvedClassReflection = null; + if ($ancestor !== null && $ancestor->hasProperty($propertyName)->yes()) { + $resolvedClassReflection = $ancestor->getClassReflection(); + if ($ancestor !== $this) { + $property = $ancestor->getUnresolvedPropertyPrototype($propertyName, $scope)->getNakedProperty(); + } + } + if ($resolvedClassReflection === null) { + $resolvedClassReflection = $property->getDeclaringClass(); + } + + return self::$properties[$description][$propertyName][$canAccessProperty] = new CalledOnTypeUnresolvedPropertyPrototypeReflection( + $property, + $resolvedClassReflection, + true, + $this, + ); + } + + public function getPropertyWithoutTransformingStatic(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + { + $classReflection = $this->getNakedClassReflection(); if ($classReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } - if ($classReflection->isGeneric() && static::class === self::class) { - return $this->getGenericObjectType()->getProperty($propertyName, $scope); + if (!$classReflection->hasProperty($propertyName)) { + $classReflection = $this->getClassReflection(); + } + + if ($classReflection === null) { + throw new ClassNotFoundException($this->className); } 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->getBaseClass()); + return $this->checkSubclassAcceptability($type->getClassName()); } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ClosureType) { - return $this->isInstanceOf(\Closure::class); + return 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 { - $description = $type->describe(VerbosityLevel::cache()); - if (isset($this->superTypes[$description])) { - return $this->superTypes[$description]; + $thatClassNames = $type->getObjectClassNames(); + if (!$type instanceof CompoundType && $thatClassNames === [] && !$type instanceof ObjectWithoutClassType) { + return IsSuperTypeOfResult::createNo(); + } + + $thisDescription = $this->describeCache(); + + if ($type instanceof self) { + $description = $type->describeCache(); + } else { + $description = $type->describe(VerbosityLevel::cache()); + } + + if (isset(self::$superTypes[$thisDescription][$description])) { + return self::$superTypes[$thisDescription][$description]; } if ($type instanceof CompoundType) { - return $this->superTypes[$description] = $type->isSubTypeOf($this); + 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 $this->superTypes[$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } } - return $this->superTypes[$description] = TrinaryLogic::createMaybe(); - } - - if (!$type instanceof TypeWithClassName) { - return $this->superTypes[$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 $this->superTypes[$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); + } + if ($isSuperType->maybe()) { + $transformResult = static fn (IsSuperTypeOfResult $result) => $result->and(IsSuperTypeOfResult::createMaybe()); } } @@ -169,47 +371,69 @@ public function isSuperTypeOf(Type $type): TrinaryLogic ) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { - return $this->superTypes[$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 $this->superTypes[$description] = TrinaryLogic::createYes(); + $thisClassReflection = $this->getClassReflection(); + $thatClassReflections = $type->getObjectClassReflections(); + if (count($thatClassReflections) === 1) { + $thatClassReflection = $thatClassReflections[0]; + } else { + $thatClassReflection = null; + } + + if ($thisClassReflection === null || $thatClassReflection === null) { + if ($thatClassNames[0] === $thisClassName) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); + } + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - $broker = Broker::getInstance(); + 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 || !$broker->hasClass($thatClassName)) { - return $this->superTypes[$description] = TrinaryLogic::createMaybe(); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - $thisClassReflection = $this->getClassReflection(); - $thatClassReflection = $broker->getClass($thatClassName); + if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); + } if ($thisClassReflection->getName() === $thatClassReflection->getName()) { - return $this->superTypes[$description] = TrinaryLogic::createYes(); + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thatClassReflection->isSubclassOf($thisClassName)) { - return $this->superTypes[$description] = TrinaryLogic::createYes(); + if ($thatClassReflection->isSubclassOfClass($thisClassReflection)) { + return self::$superTypes[$thisDescription][$description] = $transformResult(IsSuperTypeOfResult::createYes()); } - if ($thisClassReflection->isSubclassOf($thatClassName)) { - return $this->superTypes[$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection->isSubclassOfClass($thatClassReflection)) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thisClassReflection->isInterface() && !$thatClassReflection->getNativeReflection()->isFinal()) { - return $this->superTypes[$description] = TrinaryLogic::createMaybe(); + if ($thisClassReflection->isInterface() && !$thatClassReflection->isFinalByKeyword()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - if ($thatClassReflection->isInterface() && !$thisClassReflection->getNativeReflection()->isFinal()) { - return $this->superTypes[$description] = TrinaryLogic::createMaybe(); + if ($thatClassReflection->isInterface() && !$thisClassReflection->isFinalByKeyword()) { + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createMaybe(); } - return $this->superTypes[$description] = TrinaryLogic::createNo(); + return self::$superTypes[$thisDescription][$description] = IsSuperTypeOfResult::createNo(); } public function equals(Type $type): bool @@ -218,16 +442,16 @@ public function equals(Type $type): bool return false; } + if ($type instanceof EnumCaseObjectType) { + return false; + } + if ($this->className !== $type->className) { return false; } if ($this->subtractedType === null) { - if ($type->subtractedType === null) { - return true; - } - - return false; + return $type->subtractedType === null; } if ($type->subtractedType === null) { @@ -237,61 +461,123 @@ public function equals(Type $type): bool return $this->subtractedType->equals($type->subtractedType); } - protected function checkSubclassAcceptability(string $thatClass): TrinaryLogic + private function checkSubclassAcceptability(string $thatClass): AcceptsResult { if ($this->className === $thatClass) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($this->getClassReflection() === null || !$broker->hasClass($thatClass)) { - return TrinaryLogic::createNo(); + if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClass)) { + return AcceptsResult::createNo(); } $thisReflection = $this->getClassReflection(); - $thatReflection = $broker->getClass($thatClass); + $thatReflection = $reflectionProvider->getClass($thatClass); if ($thisReflection->getName() === $thatReflection->getName()) { // class alias - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($thisReflection->isInterface() && $thatReflection->isInterface()) { - return TrinaryLogic::createFromBoolean( - $thatReflection->getNativeReflection()->implementsInterface($this->className) + return AcceptsResult::createFromBoolean( + $thatReflection->implementsInterface($thisReflection->getName()), ); } - return TrinaryLogic::createFromBoolean( - $thatReflection->isSubclassOf($this->className) + return AcceptsResult::createFromBoolean( + $thatReflection->isSubclassOfClass($thisReflection), ); } public function describe(VerbosityLevel $level): string { $preciseNameCallback = function (): string { - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->className)) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->className)) { return $this->className; } - return $broker->getClassName($this->className); + return $reflectionProvider->getClassName($this->className); + }; + + $preciseWithSubtracted = function () use ($level): string { + $description = $this->className; + if ($this->subtractedType !== null) { + $description .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe($level)) + : sprintf('~%s', $this->subtractedType->describe($level)); + } + + return $description; }; + return $level->handle( $preciseNameCallback, $preciseNameCallback, - function () use ($level): string { - $description = $this->className; - if ($this->subtractedType !== null) { - $description .= sprintf('~%s', $this->subtractedType->describe($level)); + $preciseWithSubtracted, + function () use ($preciseWithSubtracted): string { + $reflection = $this->classReflection; + $line = ''; + if ($reflection !== null) { + $line .= '-'; + $line .= (string) $reflection->getNativeReflection()->getStartLine(); + $line .= '-'; } - return $description; - } + return $preciseWithSubtracted() . '-' . static::class . '-' . $line . $this->describeAdditionalCacheKey(); + }, ); } + protected function describeAdditionalCacheKey(): string + { + return ''; + } + + private function describeCache(): string + { + if ($this->cachedDescription !== null) { + return $this->cachedDescription; + } + + if (static::class !== self::class) { + 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 .= $this->subtractedType instanceof UnionType + ? sprintf('~(%s)', $this->subtractedType->describe(VerbosityLevel::cache())) + : sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); + } + + $reflection = $this->classReflection; + if ($reflection !== null) { + $description .= '-'; + $description .= (string) $reflection->getNativeReflection()->getStartLine(); + $description .= '-'; + + if ($reflection->hasFinalByKeywordOverride()) { + $description .= 'f=' . ($reflection->isFinalByKeyword() ? 't' : 'f'); + } + } + + return $this->cachedDescription = $description; + } + public function toNumber(): Type { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { @@ -304,12 +590,21 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { return new IntegerType(); } + if (in_array($this->getClassName(), ['CurlHandle', 'CurlMultiHandle'], true)) { + return new IntegerType(); + } + return new ErrorType(); } @@ -323,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($classReflection->getNativeMethod('__toString')->getVariants())->getReturnType(); + return $this->getMethod('__toString', new OutOfClassScope())->getOnlyVariant()->getReturnType(); } return new ErrorType(); @@ -342,14 +645,14 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if ( !$classReflection->getNativeReflection()->isUserDefined() + || $classReflection->is(ArrayObject::class) || UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $broker, - $broker->getUniversalObjectCratesClasses(), - $classReflection + $reflectionProvider, + $classReflection, ) ) { return new ArrayType(new MixedType(), new MixedType()); @@ -357,13 +660,15 @@ public function toArray(): Type $arrayKeys = []; $arrayValues = []; + $isFinal = $classReflection->isFinal(); + do { foreach ($classReflection->getNativeReflection()->getProperties() as $nativeProperty) { if ($nativeProperty->isStatic()) { continue; } - $declaringClass = $broker->getClass($nativeProperty->getDeclaringClass()->getName()); + $declaringClass = $reflectionProvider->getClass($nativeProperty->getDeclaringClass()->getName()); $property = $declaringClass->getNativeProperty($nativeProperty->getName()); $keyName = $nativeProperty->getName(); @@ -371,12 +676,12 @@ public function toArray(): Type $keyName = sprintf( "\0%s\0%s", $declaringClass->getName(), - $keyName + $keyName, ); } elseif ($nativeProperty->isProtected()) { $keyName = sprintf( "\0*\0%s", - $keyName + $keyName, ); } @@ -385,11 +690,80 @@ public function toArray(): Type } $classReflection = $classReflection->getParentClass(); - } while ($classReflection !== false); + } 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() + || $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(); @@ -422,39 +796,56 @@ 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 { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection + { + if (!$scope->isInClass()) { + $canCallMethod = 'no'; + } else { + $canCallMethod = $scope->getClassReflection()->getName(); + } + $description = $this->describeCache(); + if (isset(self::$methods[$description][$methodName][$canCallMethod])) { + return self::$methods[$description][$methodName][$canCallMethod]; } - if ($classReflection->isGeneric() && static::class === self::class) { - return $this->getGenericObjectType()->getMethod($methodName, $scope); + $nakedClassReflection = $this->getNakedClassReflection(); + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); } - return new ObjectTypeMethodReflection( - $this, - $classReflection->getMethod($methodName, $scope) - ); - } + if (!$nakedClassReflection->hasNativeMethod($methodName)) { + $nakedClassReflection = $this->getClassReflection(); + } - private function getGenericObjectType(): GenericObjectType - { - $classReflection = $this->getClassReflection(); - if ($classReflection === null || !$classReflection->isGeneric()) { - throw new \PHPStan\ShouldNotHappenException(); + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); } - if ($this->genericObjectType === null) { - $this->genericObjectType = new GenericObjectType( - $this->className, - array_values($classReflection->getTemplateTypeMap()->resolveToBounds()->getTypes()), - $this->subtractedType - ); + $method = $nakedClassReflection->getMethod($methodName, $scope); + + $ancestor = $this->getAncestorWithClassName($method->getDeclaringClass()->getName()); + $resolvedClassReflection = null; + if ($ancestor !== null) { + $resolvedClassReflection = $ancestor->getClassReflection(); + if ($ancestor !== $this) { + $method = $ancestor->getUnresolvedMethodPrototype($methodName, $scope)->getNakedMethod(); + } + } + if ($resolvedClassReflection === null) { + $resolvedClassReflection = $method->getDeclaringClass(); } - return $this->genericObjectType; + return self::$methods[$description][$methodName][$canCallMethod] = new CalledOnTypeUnresolvedMethodPrototypeReflection( + $method, + $resolvedClassReflection, + true, + $this, + ); } public function canAccessConstants(): TrinaryLogic @@ -470,101 +861,296 @@ public function hasConstant(string $constantName): TrinaryLogic } return TrinaryLogic::createFromBoolean( - $class->hasConstant($constantName) + $class->hasConstant($constantName), ); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { $class = $this->getClassReflection(); if ($class === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } return $class->getConstant($constantName); } - public function isIterable(): TrinaryLogic - { - return $this->isInstanceOf(\Traversable::class); - } - - public function isIterableAtLeastOnce(): TrinaryLogic - { - return $this->isInstanceOf(\Traversable::class) - ->and(TrinaryLogic::createMaybe()); - } - - public function getIterableKeyType(): Type + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type { $classReflection = $this->getClassReflection(); if ($classReflection === null) { return new ErrorType(); } - if ($this->isInstanceOf(\Iterator::class)->yes()) { - return ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod('key')->getVariants())->getReturnType(); + $ancestorClassReflection = $classReflection->getAncestorWithClassName($ancestorClassName); + if ($ancestorClassReflection === null) { + return new ErrorType(); } - if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) { - return RecursionGuard::run($this, static function () use ($classReflection): Type { - return ParametersAcceptorSelector::selectSingle( - $classReflection->getNativeMethod('getIterator')->getVariants() - )->getReturnType()->getIterableKeyType(); - }); + $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; + } - if ($this->isInstanceOf(\Traversable::class)->yes()) { - $tKey = GenericTypeVariableResolver::getType($this, \Traversable::class, 'TKey'); - if ($tKey !== null) { - return $tKey; + $bound = TemplateTypeHelper::resolveToBounds($templateType); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + return new MixedType(false); } - return new MixedType(); + return TemplateTypeHelper::resolveToDefaults($templateType); } - return new ErrorType(); + return $type; } - public function getIterableValueType(): Type + public function getConstantStrings(): array { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { + return []; + } + + public function isIterable(): TrinaryLogic + { + return $this->isInstanceOf(Traversable::class); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return $this->isInstanceOf(Traversable::class) + ->and(TrinaryLogic::createMaybe()); + } + + public function getArraySize(): Type + { + if ($this->isInstanceOf(Countable::class)->no()) { return new ErrorType(); } - if ($this->isInstanceOf(\Iterator::class)->yes()) { - return ParametersAcceptorSelector::selectSingle( - $classReflection->getNativeMethod('current')->getVariants() - )->getReturnType(); - } + return IntegerRangeType::fromInterval(0, null); + } - if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) { - return RecursionGuard::run($this, static function () use ($classReflection): Type { - return ParametersAcceptorSelector::selectSingle( - $classReflection->getNativeMethod('getIterator')->getVariants() - )->getReturnType()->getIterableValueType(); - }); + public function getIterableKeyType(): Type + { + $isTraversable = false; + if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { + $keyType = RecursionGuard::run($this, fn (): Type => $this->getMethod('getIterator', new OutOfClassScope())->getOnlyVariant()->getReturnType()->getIterableKeyType()); + $isTraversable = true; + if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) { + return $keyType; + } } - if ($this->isInstanceOf(\Traversable::class)->yes()) { - $tValue = GenericTypeVariableResolver::getType($this, \Traversable::class, 'TValue'); - if ($tValue !== null) { - return $tValue; + $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 => $this->getMethod('key', new OutOfClassScope())->getOnlyVariant()->getReturnType()); + } + if ($extraOffsetAccessible) { + return new MixedType(true); + } + + if ($isTraversable) { return new MixedType(); } return new ErrorType(); } - public function isArray(): TrinaryLogic + public function getFirstIterableKeyType(): Type { - return TrinaryLogic::createNo(); + return $this->getIterableKeyType(); } - private function isExtraOffsetAccessibleClass(): TrinaryLogic + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getIterableValueType(): Type + { + $isTraversable = false; + if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { + $valueType = RecursionGuard::run($this, fn (): Type => $this->getMethod('getIterator', new OutOfClassScope())->getOnlyVariant()->getReturnType()->getIterableValueType()); + $isTraversable = true; + if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) { + return $valueType; + } + } + + $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 => $this->getMethod('current', new OutOfClassScope())->getOnlyVariant()->getReturnType()); + } + + if ($extraOffsetAccessible) { + return new MixedType(true); + } + + if ($isTraversable) { + return new MixedType(); + } + + return new ErrorType(); + } + + 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(); + } + + 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 $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(); if ($classReflection === null) { @@ -572,39 +1158,44 @@ 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(); } } - return TrinaryLogic::createNo(); + if ($classReflection->isInterface()) { + return TrinaryLogic::createMaybe(); + } + + if ($classReflection->isFinal()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); } public function isOffsetAccessible(): TrinaryLogic { - return $this->isInstanceOf(\ArrayAccess::class)->or( - $this->isExtraOffsetAccessibleClass() + return $this->isInstanceOf(ArrayAccess::class)->or( + $this->isExtraOffsetAccessibleClass(), ); } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isOffsetAccessLegal(): TrinaryLogic { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { - return TrinaryLogic::createNo(); - } + return $this->isOffsetAccessible(); + } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { - $acceptedOffsetType = RecursionGuard::run($this, function () use ($classReflection): Type { - $parameters = ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod('offsetSet')->getVariants())->getParameters(); + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + $acceptedOffsetType = RecursionGuard::run($this, function (): Type { + $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); if (count($parameters) < 2) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', $this->className, - 'offsetSet' + 'offsetSet', )); } @@ -626,43 +1217,32 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { - return new ErrorType(); - } - if (!$this->isExtraOffsetAccessibleClass()->no()) { return new MixedType(); } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { - return RecursionGuard::run($this, static function () use ($classReflection): Type { - return ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod('offsetGet')->getVariants())->getReturnType(); - }); + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + return RecursionGuard::run($this, fn (): Type => $this->getMethod('offsetGet', new OutOfClassScope())->getOnlyVariant()->getReturnType()); } return new ErrorType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($this->isOffsetAccessible()->no()) { return new ErrorType(); } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { - return new ErrorType(); - } + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedValueType = new NeverType(); - $acceptedOffsetType = RecursionGuard::run($this, function () use ($classReflection, &$acceptedValueType): Type { - $parameters = ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod('offsetSet')->getVariants())->getParameters(); + $acceptedOffsetType = RecursionGuard::run($this, function () use (&$acceptedValueType): Type { + $parameters = $this->getMethod('offsetSet', new OutOfClassScope())->getOnlyVariant()->getParameters(); if (count($parameters) < 2) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', $this->className, - 'offsetSet' + 'offsetSet', )); } @@ -688,12 +1268,75 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType): Type 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()) { + return new ErrorType(); + } + + 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 @@ -705,25 +1348,21 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - if ($this->className === \Closure::class) { - return [new TrivialParametersAcceptor()]; + if ($this->className === Closure::class) { + return [new TrivialParametersAcceptor('Closure')]; } $parametersAcceptors = $this->findCallableParametersAcceptors(); if ($parametersAcceptors === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $parametersAcceptors; } /** - * @return \PHPStan\Reflection\ParametersAcceptor[]|null + * @return CallableParametersAcceptor[]|null */ private function findCallableParametersAcceptors(): ?array { @@ -733,10 +1372,14 @@ private function findCallableParametersAcceptors(): ?array } if ($classReflection->hasNativeMethod('__invoke')) { - return $classReflection->getNativeMethod('__invoke')->getVariants(); + $method = $this->getMethod('__invoke', new OutOfClassScope()); + return FunctionCallableVariant::createFromVariants( + $method, + $method->getVariants(), + ); } - if (!$classReflection->getNativeReflection()->isFinal()) { + if (!$classReflection->isFinalByKeyword()) { return [new TrivialParametersAcceptor()]; } @@ -748,18 +1391,6 @@ public function isCloneable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @param mixed[] $properties - * @return Type - */ - 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(); @@ -767,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(); } @@ -794,6 +1433,55 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { + 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; + } + } + + return new self($this->className, $subtractedType); + } + + if (count($allowedSubTypes) === 1) { + return array_values($allowedSubTypes)[0]; + } + + $subtractedSubTypes = array_values($subtractedSubTypes); + $subtractedSubTypesCount = count($subtractedSubTypes); + if ($subtractedSubTypesCount === count($originalAllowedSubTypes)) { + return new NeverType(); + } + + if ($subtractedSubTypesCount === 0) { + return new self($this->className); + } + + if ($subtractedSubTypesCount === 1) { + return new self($this->className, $subtractedSubTypes[0]); + } + + return new self($this->className, new UnionType($subtractedSubTypes)); + } + } + + if ($this->subtractedType === null && $subtractedType === null) { + return $this; + } + return new self($this->className, $subtractedType); } @@ -809,55 +1497,95 @@ public function traverse(callable $cb): Type if ($subtractedType !== $this->subtractedType) { return new self( $this->className, - $subtractedType + $subtractedType, ); } 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) { + return $this->classReflection; + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->className)) { + return null; + } + + return $reflectionProvider->getClass($this->className); + } + public function getClassReflection(): ?ClassReflection { if ($this->classReflection !== null) { return $this->classReflection; } - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->className)) { + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->className)) { return null; } - $classReflection = $broker->getClass($this->className); + $classReflection = $reflectionProvider->getClass($this->className); if ($classReflection->isGeneric()) { - return $this->classReflection = $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->resolveToBounds()->getTypes())); + return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes())); } - return $this->classReflection = $classReflection; + return $classReflection; } - /** - * @param string $className - * @return self|null - */ - public function getAncestorWithClassName(string $className): ?ObjectType + public function getAncestorWithClassName(string $className): ?self { - $broker = Broker::getInstance(); - if (!$broker->hasClass($className)) { - return null; + if ($this->className === $className) { + return $this; + } + + if ($this->classReflection !== null && $className === $this->classReflection->getName()) { + return $this; + } + + if (array_key_exists($className, $this->currentAncestors)) { + return $this->currentAncestors[$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 self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } - $theirReflection = $broker->getClass($className); + $theirReflection = $reflectionProvider->getClass($className); + $thisReflection = $this->getClassReflection(); if ($thisReflection === null) { - return null; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } - if ($theirReflection->getName() === $thisReflection->getName()) { - return $this; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $this; } foreach ($this->getInterfaces() as $interface) { $ancestor = $interface->getAncestorWithClassName($className); if ($ancestor !== null) { - return $ancestor; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $ancestor; } } @@ -865,39 +1593,89 @@ public function getAncestorWithClassName(string $className): ?ObjectType if ($parent !== null) { $ancestor = $parent->getAncestorWithClassName($className); if ($ancestor !== null) { - return $ancestor; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $ancestor; } } - return null; + return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null; } private function getParent(): ?ObjectType { + if ($this->cachedParent !== null) { + return $this->cachedParent; + } $thisReflection = $this->getClassReflection(); if ($thisReflection === null) { return null; } $parentReflection = $thisReflection->getParentClass(); - if ($parentReflection === false) { + if ($parentReflection === null) { return null; } - return self::createFromReflection($parentReflection); + return $this->cachedParent = self::createFromReflection($parentReflection); } /** @return ObjectType[] */ private function getInterfaces(): array { + if ($this->cachedInterfaces !== null) { + return $this->cachedInterfaces; + } $thisReflection = $this->getClassReflection(); if ($thisReflection === null) { - return []; + return $this->cachedInterfaces = []; + } + + return $this->cachedInterfaces = array_map(static fn (ClassReflection $interfaceReflection): self => self::createFromReflection($interfaceReflection), $thisReflection->getInterfaces()); + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ObjectType) { + foreach (UnionType::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if ($this->getClassName() !== $baseClass) { + continue; + } + + 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), + ); + } + } + } + } + + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + 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(); + } - return array_map(static function (ClassReflection $interfaceReflection): self { - return self::createFromReflection($interfaceReflection); - }, $thisReflection->getInterfaces()); + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->getClassName()); } } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index 223af7168e..3abe064e3f 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -2,45 +2,65 @@ 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; use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function sprintf; +/** @api */ class ObjectWithoutClassType implements SubtractableType { use ObjectTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; - private ?\PHPStan\Type\Type $subtractedType; + private ?Type $subtractedType; + /** @api */ public function __construct( - ?Type $subtractedType = null + ?Type $subtractedType = null, ) { + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + $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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + 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); @@ -48,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 @@ -95,23 +119,31 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'object'; - }, - static function (): string { - return 'object'; - }, + static fn (): string => 'object', + static fn (): string => 'object', function () use ($level): string { $description = 'object'; if ($this->subtractedType !== null) { - $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; - } + }, ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getEnumCases(): array + { + return []; + } + public function subtract(Type $type): Type { if ($type instanceof self) { @@ -139,7 +171,6 @@ public function getSubtractedType(): ?Type return $this->subtractedType; } - public function traverse(callable $cb): Type { $subtractedType = $this->subtractedType !== null ? $cb($this->subtractedType) : null; @@ -151,13 +182,44 @@ public function traverse(callable $cb): Type return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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()) { + return $this->subtract($typeToRemove); + } + + return null; + } + + public function exponentiate(Type $exponent): Type + { + if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode { - return new 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 7766086e90..a9d2fbe129 100644 --- a/src/Type/OperatorTypeSpecifyingExtension.php +++ b/src/Type/OperatorTypeSpecifyingExtension.php @@ -2,6 +2,25 @@ namespace PHPStan\Type; +/** + * 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 a1624cbe5e..7b84648f28 100644 --- a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php +++ b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php @@ -2,31 +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 { - /** @var OperatorTypeSpecifyingExtension[] */ - private array $extensions; - /** - * @param \PHPStan\Type\OperatorTypeSpecifyingExtension[] $extensions + * @param OperatorTypeSpecifyingExtension[] $extensions */ public function __construct( - Broker $broker, - array $extensions + private array $extensions, ) { - foreach ($extensions as $extension) { - if (!$extension instanceof BrokerAwareExtension) { - continue; - } - - $extension->setBroker($broker); - } - $this->extensions = $extensions; } /** @@ -34,9 +22,7 @@ public function __construct( */ public function getOperatorTypeSpecifyingExtensions(string $operator, Type $leftType, Type $rightType): array { - return array_values(array_filter($this->extensions, static function (OperatorTypeSpecifyingExtension $extension) use ($operator, $leftType, $rightType): bool { - return $extension->isOperatorSupported($operator, $leftType, $rightType); - })); + return array_values(array_filter($this->extensions, static fn (OperatorTypeSpecifyingExtension $extension): bool => $extension->isOperatorSupported($operator, $leftType, $rightType))); } } diff --git a/src/Type/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 new file mode 100644 index 0000000000..e616fffc0e --- /dev/null +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -0,0 +1,102 @@ +getName(); + } elseif ( + $lowercasedClassName === 'parent' + && $classReflection !== null + && $classReflection->getParentClass() !== null + ) { + $typeClassName = $classReflection->getParentClass()->getName(); + } + + return new ObjectType($typeClassName); + } elseif ($type instanceof NullableType) { + return TypeCombinator::addNull(self::resolve($type->type, $classReflection)); + } elseif ($type instanceof Node\UnionType) { + $types = []; + foreach ($type->types as $unionTypeType) { + $types[] = self::resolve($unionTypeType, $classReflection); + } + + return TypeCombinator::union(...$types); + } elseif ($type instanceof Node\IntersectionType) { + $types = []; + foreach ($type->types as $intersectionTypeType) { + $innerType = self::resolve($intersectionTypeType, $classReflection); + if (!$innerType->isObject()->yes()) { + return new NeverType(); + } + + $types[] = $innerType; + } + + return TypeCombinator::intersect(...$types); + } elseif (!$type instanceof Identifier) { + throw new ShouldNotHappenException(get_class($type)); + } + + $type = $type->name; + if ($type === 'string') { + return new StringType(); + } elseif ($type === 'int') { + return new IntegerType(); + } elseif ($type === 'bool') { + return new BooleanType(); + } elseif ($type === 'float') { + return new FloatType(); + } elseif ($type === 'callable') { + return new CallableType(); + } elseif ($type === 'array') { + return new ArrayType(new MixedType(), new MixedType()); + } elseif ($type === 'iterable') { + return new IterableType(new MixedType(), new MixedType()); + } elseif ($type === 'void') { + 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') { + return new NullType(); + } elseif ($type === 'mixed') { + return new MixedType(true); + } elseif ($type === 'never') { + 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 07a64c395b..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\TypeUtils; +use PHPStan\Type\TypeCombinator; +use function array_key_exists; -class ArgumentBasedFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var int[] */ - private array $functionNames = [ + private const FUNCTION_NAMES = [ 'array_unique' => 0, - 'array_reverse' => 0, - 'array_change_key_case' => 0, 'array_diff_assoc' => 0, 'array_diff_key' => 0, 'array_diff_uassoc' => 0, @@ -27,42 +27,45 @@ class ArgumentBasedFunctionReturnTypeExtension implements \PHPStan\Type\DynamicF '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, 'array_uintersect_assoc' => 0, 'array_uintersect_uassoc' => 0, 'array_uintersect' => 0, - 'iterator_to_array' => 0, ]; 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->args[$argumentPosition])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[$argumentPosition])) { + return null; } - $argument = $functionCall->args[$argumentPosition]; + $argument = $functionCall->getArgs()[$argumentPosition]; $argumentType = $scope->getType($argument->value); $argumentKeyType = $argumentType->getIterableKeyType(); $argumentValueType = $argumentType->getIterableValueType(); if ($argument->unpack) { - $argumentKeyType = TypeUtils::generalizeType($argumentKeyType); - $argumentValueType = TypeUtils::generalizeType($argumentValueType->getIterableValueType()); + $argumentKeyType = $argumentKeyType->generalize(GeneralizePrecision::moreSpecific()); + $argumentValueType = $argumentValueType->getIterableValueType()->generalize(GeneralizePrecision::moreSpecific()); } - return new ArrayType( + $array = new ArrayType( $argumentKeyType, - $argumentValueType + $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 new file mode 100644 index 0000000000..70f5892c2b --- /dev/null +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -0,0 +1,48 @@ +getName() === 'array_column'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { + 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 = $arrayType->getConstantArrays(); + if (count($constantArrayTypes) === 1) { + $type = $this->arrayColumnHelper->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); + if ($type !== null) { + return $type; + } + } + + return $this->arrayColumnHelper->handleAnyArray($arrayType, $columnType, $indexType, $scope); + } + +} diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php new file mode 100644 index 0000000000..a0796febf4 --- /dev/null +++ b/src/Type/Php/ArrayColumnHelper.php @@ -0,0 +1,196 @@ +isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return [new NeverType(), $iterableAtLeastOnce]; + } + + $iterableValueType = $arrayType->getIterableValueType(); + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + + if ($returnValueType === null) { + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, true); + $iterableAtLeastOnce = TrinaryLogic::createMaybe(); + if ($returnValueType === null) { + throw new ShouldNotHappenException(); + } + } + + return [$returnValueType, $iterableAtLeastOnce]; + } + + public function getReturnIndexType(Type $arrayType, ?Type $indexType, Scope $scope): Type + { + if ($indexType !== null) { + $iterableValueType = $arrayType->getIterableValueType(); + + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + return $type; + } + + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + return TypeCombinator::union($type, new IntegerType()); + } + } + + return new IntegerType(); + } + + public function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexType, Scope $scope): Type + { + [$returnValueType, $iterableAtLeastOnce] = $this->getReturnValueType($arrayType, $columnType, $scope); + if ($returnValueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + $returnKeyType = $this->getReturnIndexType($arrayType, $indexType, $scope); + $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); + + if ($iterableAtLeastOnce->yes()) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + if ($indexType === null) { + $returnType = TypeCombinator::intersect($returnType, new AccessoryArrayListType()); + } + + return $returnType; + } + + public function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, ?Type $indexType, Scope $scope): ?Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + 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); + if ($type !== null) { + $keyType = $type; + } else { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + $keyType = TypeCombinator::union($type, new IntegerType()); + } else { + $keyType = null; + } + } + } else { + $keyType = null; + } + + if ($keyType !== null) { + $keyType = $this->castToArrayKeyType($keyType); + } + $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); + } + + return $builder->getArray(); + } + + private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type + { + $offsetIsNull = $offsetOrProperty->isNull(); + if ($offsetIsNull->yes()) { + return $type; + } + + $returnTypes = []; + + if ($offsetIsNull->maybe()) { + $returnTypes[] = $type; + } + + if (!$type->canAccessProperties()->no()) { + $propertyTypes = $offsetOrProperty->getConstantStrings(); + if ($propertyTypes === []) { + return new MixedType(); + } + foreach ($propertyTypes as $propertyType) { + $propertyName = $propertyType->getValue(); + $hasProperty = $type->hasProperty($propertyName); + if ($hasProperty->maybe()) { + return $allowMaybe ? new MixedType() : null; + } + if (!$hasProperty->yes()) { + continue; + } + + $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); + } + } + + if ($type->isOffsetAccessible()->yes()) { + $hasOffset = $type->hasOffsetValueType($offsetOrProperty); + if (!$allowMaybe && $hasOffset->maybe()) { + return null; + } + if (!$hasOffset->no()) { + $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); + } + } + + if ($returnTypes === []) { + return new NeverType(); + } + + return TypeCombinator::union(...$returnTypes); + } + + private function castToArrayKeyType(Type $type): Type + { + $isArray = $type->isArray(); + if ($isArray->yes()) { + return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); + } + if ($isArray->no()) { + return $type->toArrayKey(); + } + $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); + $keyType = $withoutArrayType->toArrayKey(); + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $keyType; + } + return TypeCombinator::union($keyType, new IntegerType()); + } + +} diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..f809967ad8 --- /dev/null +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -0,0 +1,139 @@ +getName() === 'array_combine'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $firstArg = $functionCall->getArgs()[0]->value; + $secondArg = $functionCall->getArgs()[1]->value; + + $keysParamType = $scope->getType($firstArg); + $valuesParamType = $scope->getType($secondArg); + + if ( + $keysParamType instanceof ConstantArrayType + && $valuesParamType instanceof ConstantArrayType + ) { + $keyTypes = $keysParamType->getValueTypes(); + $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) { + $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( + $keyType, + $valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(), + ); + + if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $arrayType; + } + + if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) { + return $arrayType; + } + + return new UnionType([$arrayType, new ConstantBooleanType(false)]); + } + + /** + * @param array $types + * + * @return array|null + */ + 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 + ) { + return null; + } + + $sanitizedTypes[] = $type; + } + + return $sanitizedTypes; + } + +} diff --git a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..9871a469c9 --- /dev/null +++ b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php @@ -0,0 +1,41 @@ +getName() === 'current'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return new ConstantBooleanType(false); + } + + $keyType = $argType->getIterableValueType(); + if ($iterableAtLeastOnce->yes()) { + return $keyType; + } + + return TypeCombinator::union($keyType, new ConstantBooleanType(false)); + } + +} diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 8b1819d57f..fbadbac593 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -4,47 +4,67 @@ 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\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; -class ArrayFillFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->args) < 3) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) < 3) { + return null; + } + + $numberType = $scope->getType($functionCall->getArgs()[1]->value); + $isValidNumberType = IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($numberType); + + // check against negative-int, which is not allowed + if ($isValidNumberType->no()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); } - $startIndexType = $scope->getType($functionCall->args[0]->value); - $numberType = $scope->getType($functionCall->args[1]->value); - $valueType = $scope->getType($functionCall->args[2]->value); + $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); + $valueType = $scope->getType($functionCall->getArgs()[2]->value); if ( $startIndexType instanceof ConstantIntegerType && $numberType instanceof ConstantIntegerType - && $numberType->getValue() <= static::MAX_SIZE_USE_CONSTANT_ARRAY + && $numberType->getValue() <= self::MAX_SIZE_USE_CONSTANT_ARRAY ) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); $nextIndex = $startIndexType->getValue(); for ($i = 0; $i < $numberType->getValue(); $i++) { $arrayBuilder->setOffsetValueType( new ConstantIntegerType($nextIndex), - $valueType + $valueType, ); if ($nextIndex < 0) { $nextIndex = 0; @@ -56,17 +76,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $arrayBuilder->getArray(); } - if ( - $numberType instanceof ConstantIntegerType - && $numberType->getValue() > 0 - ) { - return new IntersectionType([ - new ArrayType(new IntegerType(), $valueType), - new NonEmptyArrayType(), - ]); + $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()) { + $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 28ffedb197..b8c7fdfe97 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -4,45 +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 \PHPStan\Type\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->args) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - $valueType = $scope->getType($functionCall->args[1]->value); - $keysType = $scope->getType($functionCall->args[0]->value); - $constantArrays = TypeUtils::getConstantArrays($keysType); - if (count($constantArrays) === 0) { - return new ArrayType($keysType->getIterableValueType(), $valueType); + if (count($functionCall->getArgs()) < 2) { + return null; } - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getValueTypes() as $keyType) { - $arrayBuilder->setOffsetValueType($keyType, $valueType); - } - $arrayTypes[] = $arrayBuilder->getArray(); + $keysType = $scope->getType($functionCall->getArgs()[0]->value); + 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 63c1fa8d65..0000000000 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ /dev/null @@ -1,128 +0,0 @@ -getName() === 'array_filter'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - $arrayArg = $functionCall->args[0]->value ?? null; - $callbackArg = $functionCall->args[1]->value ?? null; - $flagArg = $functionCall->args[2]->value ?? null; - - if ($arrayArg !== null) { - $arrayArgType = $scope->getType($arrayArg); - $keyType = $arrayArgType->getIterableKeyType(); - $itemType = $arrayArgType->getIterableValueType(); - - if ($arrayArgType instanceof MixedType) { - return new BenevolentUnionType([ - new ArrayType(new MixedType(), new MixedType()), - new NullType(), - ]); - } - - if ($callbackArg === null) { - return TypeCombinator::union( - ...array_map([$this, 'removeFalsey'], TypeUtils::getArrays($arrayArgType)) - ); - } - - if ($flagArg === null && $callbackArg instanceof Closure && count($callbackArg->stmts) === 1) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null && count($callbackArg->params) > 0) { - if (!$callbackArg->params[0]->var instanceof Variable || !is_string($callbackArg->params[0]->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $itemVariableName = $callbackArg->params[0]->var->name; - if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); - } - $scope = $scope->assignVariable($itemVariableName, $itemType); - $scope = $scope->filterByTruthyValue($statement->expr); - $itemType = $scope->getVariableType($itemVariableName); - } - } - - } else { - $keyType = new MixedType(); - $itemType = new MixedType(); - } - - return new ArrayType($keyType, $itemType); - } - - public function removeFalsey(Type $type): Type - { - $falseyTypes = new UnionType([ - new NullType(), - new ConstantBooleanType(false), - new ConstantIntegerType(0), - new ConstantFloatType(0.0), - new ConstantStringType(''), - new ConstantArrayType([], []), - ]); - - if ($type instanceof ConstantArrayType) { - $keys = $type->getKeyTypes(); - $values = $type->getValueTypes(); - - $generalize = false; - - foreach ($values as $offset => $value) { - $isFalsey = $falseyTypes->isSuperTypeOf($value); - - if ($isFalsey->yes()) { - unset($keys[$offset], $values[$offset]); - } elseif ($isFalsey->maybe()) { - $values[$offset] = TypeCombinator::remove($values[$offset], $falseyTypes); - $generalize = true; - } - } - - $filteredArray = new ConstantArrayType(array_values($keys), array_values($values)); - - return $generalize ? $filteredArray->generalize() : $filteredArray; - } - - $keyType = $type->getIterableKeyType(); - $valueType = $type->getIterableValueType(); - - $valueType = TypeCombinator::remove($valueType, $falseyTypes); - - if ($valueType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - return new ArrayType($keyType, $valueType); - } - -} 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 new file mode 100644 index 0000000000..b5b0eb1df8 --- /dev/null +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -0,0 +1,47 @@ +getName() === 'array_flip'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + 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(); + } + + $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/ArrayKeyDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php index 08cb3013dc..fe418f4daa 100644 --- a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php @@ -5,12 +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 \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -18,13 +18,13 @@ 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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[0])) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new NullType(); diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 4ca19dd4b0..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,15 +14,20 @@ 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 \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -28,11 +37,10 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { - return $functionReflection->getName() === 'array_key_exists' - && count($node->args) >= 2 + return in_array($functionReflection->getName(), ['array_key_exists', 'key_exists'], true) && !$context->null(); } @@ -40,24 +48,75 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $keyType = $scope->getType($node->args[0]->value); + if (count($node->getArgs()) < 2) { + return new SpecifiedTypes(); + } + $key = $node->getArgs()[0]->value; + $array = $node->getArgs()[1]->value; + $keyType = $scope->getType($key); + $arrayType = $scope->getType($array); + + 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->truthy()) { + if ($context->true()) { $type = TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType($keyType) + new HasOffsetType($keyType), ); } else { $type = new HasOffsetType($keyType); } return $this->typeSpecifier->create( - $node->args[1]->value, + $array, $type, - $context + $context, + $scope, ); } diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index c868e7a39e..bd9fdd6f0a 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.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; -use PHPStan\Type\TypeUtils; -class ArrayKeyFirstDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,35 +18,19 @@ 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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[0])) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { 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 25bec78100..a13714293c 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.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; -use PHPStan\Type\TypeUtils; -class ArrayKeyLastDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,35 +18,19 @@ 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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[0])) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { 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 056294ffc9..74a2903ea5 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -4,42 +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\IntegerType; -use PHPStan\Type\StringType; +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 PHPStan\Type\UnionType; +use function count; +use function strtolower; -class ArrayKeysFunctionDynamicReturnTypeExtension implements \PHPStan\Type\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->args[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 e21cdbffe0..145756b971 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -2,18 +2,28 @@ 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; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; 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 \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayMapFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,49 +31,154 @@ 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->args) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { + return null; } - $valueType = new MixedType(); - $callableType = $scope->getType($functionCall->args[0]->value); - if (!$callableType->isCallable()->no()) { - $valueType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->args, - $callableType->getCallableParametersAcceptors($scope) + $singleArrayArgument = !isset($functionCall->getArgs()[2]); + $callableType = $scope->getType($functionCall->getArgs()[0]->value); + $callableIsNull = $callableType->isNull()->yes(); + + $callableParametersAcceptors = null; + + if ($callableType->isCallable()->yes()) { + $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), + $offsetValueType, + ); + } + $valueType = $arrayBuilder->getArray(); + } else { + $valueType = new MixedType(); } - $arrayType = $scope->getType($functionCall->args[1]->value); - $constantArrays = TypeUtils::getConstantArrays($arrayType); - if (count($constantArrays) > 0) { - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $keyType) { - $returnedArrayBuilder->setOffsetValueType( - $keyType, - $valueType - ); + $arrayType = $scope->getType($functionCall->getArgs()[1]->value); + + if ($singleArrayArgument) { + if ($callableIsNull) { + return $arrayType; + } + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $arrayTypes = []; + $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; + } + + $mappedArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)); } - $arrayTypes[] = $returnedArrayBuilder->getArray(); + } elseif ($arrayType->isArray()->yes()) { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } else { + $mappedArrayType = new ArrayType( + new MixedType(), + $valueType, + ); } + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $valueType, + ), new AccessoryArrayListType(), ...TypeUtils::getAccessoryTypes($arrayType)); + } - return TypeCombinator::union(...$arrayTypes); - } elseif ($arrayType->isArray()->yes()) { - return TypeCombinator::intersect(new ArrayType( - $arrayType->getIterableKeyType(), - $valueType - ), ...TypeUtils::getAccessoryTypes($arrayType)); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + $mappedArrayType = TypeCombinator::intersect($mappedArrayType, new NonEmptyArrayType()); } - return new ArrayType( - new MixedType(), - $valueType - ); + return $mappedArrayType; } } diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 38bb6b3141..712bd4edc3 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -5,14 +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\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; +use function array_keys; +use function count; +use function in_array; -class ArrayMergeFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -20,33 +30,108 @@ 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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; } - $keyTypes = []; - $valueTypes = []; - foreach ($functionCall->args 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(), + ); + + 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(), + ); + } } - $keyTypes[] = TypeUtils::generalizeType($argType->getIterableKeyType()); + return $newArrayBuilder->getArray(); + } + + $keyTypes = []; + $valueTypes = []; + $nonEmpty = false; + $isList = true; + foreach ($argTypes as $key => $argType) { + $keyType = $argType->getIterableKeyType(); + $keyTypes[] = $keyType; $valueTypes[] = $argType->getIterableValueType(); + + if (!(new IntegerType())->isSuperTypeOf($keyType)->yes()) { + $isList = false; + } + + if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) { + continue; + } + + $nonEmpty = true; + } + + $keyType = TypeCombinator::union(...$keyTypes); + if ($keyType instanceof NeverType) { + return new ConstantArrayType([], []); } - return new ArrayType( - TypeCombinator::union(...$keyTypes), - TypeCombinator::union(...$valueTypes) + $arrayType = new ArrayType( + $keyType, + TypeCombinator::union(...$valueTypes), ); + + if ($nonEmpty) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + if ($isList) { + $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); + } + + return $arrayType; } } diff --git a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..0f33b7f532 --- /dev/null +++ b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php @@ -0,0 +1,39 @@ +getName(), ['next', 'prev'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return new ConstantBooleanType(false); + } + + $valueType = $argType->getIterableValueType(); + + return TypeCombinator::union($valueType, new ConstantBooleanType(false)); + } + +} diff --git a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php index b7e9557ded..4e49cd465f 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -5,13 +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 \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ @@ -28,40 +29,22 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) === 0) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { 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 2e26f43a2b..540dda82bb 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.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; -use PHPStan\Type\TypeUtils; -class ArrayPopFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,35 +18,19 @@ 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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[0])) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { 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 new file mode 100644 index 0000000000..1d390b59d0 --- /dev/null +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -0,0 +1,65 @@ +getName() === 'array_rand'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $argsCount = count($functionCall->getArgs()); + if ($argsCount < 1) { + return null; + } + + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + $isInteger = $firstArgType->getIterableKeyType()->isInteger(); + $isString = $firstArgType->getIterableKeyType()->isString(); + + if ($isInteger->yes()) { + $valueType = new IntegerType(); + } elseif ($isString->yes()) { + $valueType = new StringType(); + } else { + $valueType = new UnionType([new IntegerType(), new StringType()]); + } + + if ($argsCount < 2) { + return $valueType; + } + + $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); + + $one = new ConstantIntegerType(1); + if ($one->isSuperTypeOf($secondArgType)->yes()) { + return $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 ec876b45a7..970e2cb39f 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\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 \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,44 +21,44 @@ 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->args[1])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[1])) { + return null; } - $callbackType = $scope->getType($functionCall->args[1]->value); + $callbackType = $scope->getType($functionCall->getArgs()[1]->value); if ($callbackType->isCallable()->no()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackReturnType = ParametersAcceptorSelector::selectFromArgs( $scope, - $functionCall->args, - $callbackType->getCallableParametersAcceptors($scope) + $functionCall->getArgs(), + $callbackType->getCallableParametersAcceptors($scope), )->getReturnType(); - if (isset($functionCall->args[2])) { - $initialType = $scope->getType($functionCall->args[2]->value); + if (isset($functionCall->getArgs()[2])) { + $initialType = $scope->getType($functionCall->getArgs()[2]->value); } else { $initialType = new NullType(); } - $arraysType = $scope->getType($functionCall->args[0]->value); - $constantArrays = TypeUtils::getConstantArrays($arraysType); + $arraysType = $scope->getType($functionCall->getArgs()[0]->value); + $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 new file mode 100644 index 0000000000..825f7d6c1a --- /dev/null +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'array_reverse'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $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 2c56f6c47c..762e577211 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -4,160 +4,55 @@ 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->args); + $argsCount = count($functionCall->getArgs()); if ($argsCount < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $haystackArgType = $scope->getType($functionCall->args[1]->value); - $haystackIsArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($haystackArgType); - if ($haystackIsArray->no()) { - return new NullType(); + $haystackArgType = $scope->getType($functionCall->getArgs()[1]->value); + if ($haystackArgType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if ($argsCount < 3) { return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); } - $strictArgType = $scope->getType($functionCall->args[2]->value); - if (!($strictArgType instanceof ConstantBooleanType)) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false), new NullType()); - } elseif ($strictArgType->getValue() === false) { + $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); + if (!$strictArgType->isTrue()->yes()) { return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); } - $needleArgType = $scope->getType($functionCall->args[0]->value); + $needleArgType = $scope->getType($functionCall->getArgs()[0]->value); if ($haystackArgType->getIterableValueType()->isSuperTypeOf($needleArgType)->no()) { return new ConstantBooleanType(false); } - $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); - } - - /** - * @param Type $type - * @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 b7feab7eca..cb6c35bbdd 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.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; -use PHPStan\Type\TypeUtils; -class ArrayShiftFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,35 +18,19 @@ 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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[0])) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { 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 e46fa68e49..75f6a14b63 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -4,78 +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\IntegerType; -use PHPStan\Type\MixedType; +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 ArraySliceFunctionReturnTypeExtension implements \PHPStan\Type\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->args[0]->value ?? null; - - if ($arrayArg === null) { - return new ArrayType( - new IntegerType(), - new MixedType() - ); - } - - $valueType = $scope->getType($arrayArg); - - if (isset($functionCall->args[1])) { - $offset = $scope->getType($functionCall->args[1]->value); - if (!$offset instanceof ConstantIntegerType) { - $offset = new ConstantIntegerType(0); - } - } else { - $offset = new ConstantIntegerType(0); - } - - if (isset($functionCall->args[2])) { - $limit = $scope->getType($functionCall->args[2]->value); - if (!$limit instanceof ConstantIntegerType) { - $limit = new NullType(); - } - } else { - $limit = new NullType(); - } - - $constantArrays = TypeUtils::getConstantArrays($valueType); - if (count($constantArrays) === 0) { - return $valueType; + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - if (isset($functionCall->args[3])) { - $preserveKeys = $scope->getType($functionCall->args[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 function (ConstantArrayType $constantArray) use ($offset, $limit, $preserveKeys): ConstantArrayType { - return $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 new file mode 100644 index 0000000000..85def351d2 --- /dev/null +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -0,0 +1,50 @@ +getName() === 'array_splice'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $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(); + } + + $offsetType = $scope->getType($args[1]->value); + $lengthType = isset($args[2]) ? $scope->getType($args[2]->value) : new NullType(); + + return $arrayType->sliceArray($offsetType, $lengthType, TrinaryLogic::createNo()); + } + +} diff --git a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..5185fbccc1 --- /dev/null +++ b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,65 @@ +getName() === 'array_sum'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $resultTypes = []; + + if (count($argType->getConstantArrays()) > 0) { + foreach ($argType->getConstantArrays() as $constantArray) { + $node = new Int_(0); + + 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)); + } + } + + $resultTypes[] = $scope->getType($node); + } + } else { + $itemType = $argType->getIterableValueType(); + + $mulNode = new Mul(new TypeExpr($itemType), new TypeExpr(IntegerRangeType::fromInterval(0, null))); + + $resultTypes[] = $scope->getType(new Plus(new TypeExpr($itemType), $mulNode)); + } + + if (!$argType->isIterableAtLeastOnce()->yes()) { + $resultTypes[] = new ConstantIntegerType(0); + } + + return TypeCombinator::union(...$resultTypes)->toNumber(); + } + +} diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 0c43caa801..2378a291b3 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -4,40 +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\IntegerType; -use PHPStan\Type\MixedType; +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; +use function strtolower; -class ArrayValuesFunctionDynamicReturnTypeExtension implements \PHPStan\Type\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->args[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 c6412e9339..d1458a27dd 100644 --- a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php @@ -11,20 +11,20 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\FunctionTypeSpecifyingExtension; -class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return $functionReflection->getName() === 'assert' - && isset($node->args[0]); + && isset($node->getArgs()[0]); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - return $this->typeSpecifier->specifyTypesInCondition($scope, $node->args[0]->value, TypeSpecifierContext::createTruthy()); + return $this->typeSpecifier->specifyTypesInCondition($scope, $node->getArgs()[0]->value, TypeSpecifierContext::createTruthy()); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/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 dc36db80d4..6e38a43a25 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -7,12 +7,13 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class Base64DecodeDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,21 +24,21 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (!isset($functionCall->args[1])) { + if (!isset($functionCall->getArgs()[1])) { return new StringType(); } - $argType = $scope->getType($functionCall->args[1]->value); + $argType = $scope->getType($functionCall->getArgs()[1]->value); if ($argType instanceof MixedType) { return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]); } - $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 new file mode 100644 index 0000000000..2651cb6b90 --- /dev/null +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -0,0 +1,274 @@ +getName(), ['bcdiv', 'bcmod', 'bcpowmod', 'bcsqrt'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if ($functionReflection->getName() === 'bcsqrt') { + return $this->getTypeForBcSqrt($functionCall, $scope); + } + + if ($functionReflection->getName() === 'bcpowmod') { + return $this->getTypeForBcPowMod($functionCall, $scope); + } + + $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); + + if (isset($functionCall->getArgs()[1]) === false) { + 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->isInteger()->yes(); + + if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new NullType(); + } + + if (isset($functionCall->getArgs()[2]) === false) { + if ($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) { + return $stringAndNumericStringType; + } + + return $defaultReturnType; + } + + $thirdArgument = $scope->getType($functionCall->getArgs()[2]->value); + $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; + } + + return $defaultReturnType; + } + + /** + * bcsqrt + * https://www.php.net/manual/en/function.bcsqrt.php + * > Returns the square root as a string, or NULL if operand is negative. + * + */ + private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type + { + $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); + 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; + } + + $firstArgument = $scope->getType($functionCall->getArgs()[0]->value); + + $firstArgumentIsPositive = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() >= 0; + $firstArgumentIsNegative = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() < 0; + + if ($firstArgument instanceof UnaryMinus || $firstArgumentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new NullType(); + } + + if (isset($functionCall->getArgs()[1]) === false) { + if ($firstArgumentIsPositive) { + return $stringAndNumericStringType; + } + + return $defaultReturnType; + } + + $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; + } + + return $defaultReturnType; + } + + /** + * bcpowmod() + * https://www.php.net/manual/en/function.bcpowmod.php + * > Returns the result as a string, or FALSE if modulus is 0 or exponent is negative. + */ + 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) { + $exponentIsNegative = is_numeric($exponent->getValue()) && $exponent->getValue() < 0; + } + + if ($exponentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); + } + + if (isset($functionCall->getArgs()[2])) { + $modulus = $scope->getType($functionCall->getArgs()[2]->value); + $modulusIsZero = $modulus instanceof ConstantScalarType && $this->isZero($modulus->getValue()); + $modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue()); + + 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)]); + } + + /** + * Utility to help us determine if value is zero. Handles cases where we pass "0.000" too. + * + * @param mixed $value + */ + private function isZero($value): bool + { + if (is_numeric($value) === false) { + return false; + } + + if ($value > 0 || $value < 0) { + return false; + } + + return true; + } + +} diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 63d12b67e4..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\NeverType; -use PHPStan\Type\TypeCombinator; +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; @@ -27,38 +29,41 @@ class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyi public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return in_array($functionReflection->getName(), [ 'class_exists', 'interface_exists', 'trait_exists', - ], true) && isset($node->args[0]) && $context->truthy(); + '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->args[0]->value); - $classStringType = new ClassStringType(); - if (TypeCombinator::intersect($argType, $classStringType) instanceof NeverType) { - if ($argType instanceof ConstantStringType) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('class_exists'), [ - new Arg(new String_(ltrim($argType->getValue(), '\\'))), - ]), - new ConstantBooleanType(true), - $context - ); - } + $argType = $scope->getType($node->getArgs()[0]->value); + if ($argType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('class_exists'), [ + new Arg(new String_(ltrim($argType->getValue(), '\\'))), + ]), + new ConstantBooleanType(true), + $context, + $scope, + ); + } - return new SpecifiedTypes(); + $narrowedType = new ClassStringType(); + if ($functionReflection->getName() === 'enum_exists') { + $narrowedType = new GenericClassStringType(new ObjectType('UnitEnum')); } return $this->typeSpecifier->create( - $node->args[0]->value, - $classStringType, - $context + $node->getArgs()[0]->value, + $narrowedType, + $context, + $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 9f64a13b7a..719fd172d3 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -2,19 +2,20 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ClosureType; +use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Type; -class ClosureBindDynamicReturnTypeExtension implements \PHPStan\Type\DynamicStaticMethodReturnTypeExtension +final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -22,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->args[0]->value); + $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 1ea706a5ec..9e495b3163 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -2,19 +2,20 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ClosureType; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; -class ClosureBindToDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isMethodSupported(MethodReflection $methodReflection): bool @@ -22,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 8f3cf04574..d579157160 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -2,20 +2,24 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; 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 \PHPStan\Type\DynamicStaticMethodReturnTypeExtension +final class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -23,9 +27,13 @@ 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 { - $callableType = $scope->getType($methodCall->args[0]->value); + if (!isset($methodCall->getArgs()[0])) { + return null; + } + + $callableType = $scope->getType($methodCall->getArgs()[0]->value); if ($callableType->isCallable()->no()) { return new ErrorType(); } @@ -36,7 +44,16 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $closureTypes[] = new ClosureType( $parameters, $variant->getReturnType(), - $variant->isVariadic() + $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 new file mode 100644 index 0000000000..435d4b067d --- /dev/null +++ b/src/Type/Php/CompactFunctionReturnTypeExtension.php @@ -0,0 +1,88 @@ +getName() === 'compact'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + if (count($functionCall->getArgs()) === 0) { + return null; + } + + if ($scope->canAnyVariableExist() && !$this->checkMaybeUndefinedVariables) { + return null; + } + + $array = ConstantArrayTypeBuilder::createEmpty(); + foreach ($functionCall->getArgs() as $arg) { + $type = $scope->getType($arg->value); + $constantStrings = $this->findConstantStrings($type); + if ($constantStrings === null) { + return null; + } + foreach ($constantStrings as $constantString) { + $has = $scope->hasVariableType($constantString->getValue()); + if ($has->no()) { + continue; + } + + $array->setOffsetValueType($constantString, $scope->getVariableType($constantString->getValue()), $has->maybe()); + } + } + + return $array->getArray(); + } + + /** + * @return array|null + */ + private function findConstantStrings(Type $type): ?array + { + if ($type instanceof ConstantStringType) { + return [$type]; + } + + if ($type instanceof ConstantArrayType) { + $result = []; + foreach ($type->getValueTypes() as $valueType) { + $constantStrings = $this->findConstantStrings($valueType); + if ($constantStrings === null) { + return null; + } + + $result = array_merge($result, $constantStrings); + } + + return $result; + } + + return null; + } + +} 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 84f3da1717..9368cb4279 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -5,47 +5,39 @@ 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\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function count; +use function in_array; +use const COUNT_RECURSIVE; -class CountFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'count'; + return in_array($functionReflection->getName(), ['sizeof', 'count'], true); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->args) < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) < 1) { + return null; } - if (count($functionCall->args) > 1) { - $mode = $scope->getType($functionCall->args[1]->value); - if ($mode->isSuperTypeOf(new ConstantIntegerType(\COUNT_RECURSIVE))->yes()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) > 1) { + $mode = $scope->getType($functionCall->getArgs()[1]->value); + if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) { + return null; } } - $arrays = TypeUtils::getArrays($scope->getType($functionCall->args[0]->value)); - if (count($arrays) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - $countTypes = []; - foreach ($arrays 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 a760e1ff4c..b109b13f91 100644 --- a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php @@ -10,38 +10,38 @@ 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 \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return !$context->null() - && count($node->args) >= 1 - && $functionReflection->getName() === 'count'; + && count($node->getArgs()) >= 1 + && in_array($functionReflection->getName(), ['sizeof', 'count'], true); } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - if (!(new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($node->args[0]->value))->yes()) { + if (!$scope->getType($node->getArgs()[0]->value)->isArray()->yes()) { return new SpecifiedTypes([], []); } - return $this->typeSpecifier->create($node->args[0]->value, new NonEmptyArrayType(), $context); + 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 885707c877..0000000000 --- a/src/Type/Php/CurlInitReturnTypeExtension.php +++ /dev/null @@ -1,37 +0,0 @@ -getName() === 'curl_init'; - } - - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope - ): Type - { - $argsCount = count($functionCall->args); - if ($argsCount === 0) { - return new ResourceType(); - } - - return new UnionType([ - new ResourceType(), - new ConstantBooleanType(false), - ]); - } - -} diff --git a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..1a404526f3 --- /dev/null +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -0,0 +1,41 @@ +getName() === 'date_format'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + if (count($functionCall->getArgs()) < 2) { + return new StringType(); + } + + 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 new file mode 100644 index 0000000000..34854d28cc --- /dev/null +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'format'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + if (count($methodCall->getArgs()) === 0) { + return new StringType(); + } + + 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 new file mode 100644 index 0000000000..32113eb312 --- /dev/null +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -0,0 +1,40 @@ +getName() === 'date'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + if (count($functionCall->getArgs()) === 0) { + return null; + } + + 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 new file mode 100644 index 0000000000..04c356151d --- /dev/null +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -0,0 +1,64 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateInterval::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + try { + new DateInterval($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('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 new file mode 100644 index 0000000000..e20c503c05 --- /dev/null +++ b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php @@ -0,0 +1,84 @@ +getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + { + if (!isset($methodCall->getArgs()[0])) { + return new ObjectType(DatePeriod::class); + } + + if (!$methodCall->class instanceof Name) { + return new ObjectType(DatePeriod::class); + } + + $className = $scope->resolveName($methodCall->class); + if (strtolower($className) !== 'dateperiod') { + return new ObjectType($className); + } + + $firstArgType = $scope->getType($methodCall->getArgs()[0]->value); + if ($firstArgType->isString()->yes()) { + $firstArgType = new ObjectType(DateTime::class); + } + $thirdArgType = null; + if (isset($methodCall->getArgs()[2])) { + $thirdArgType = $scope->getType($methodCall->getArgs()[2]->value); + } + + if (!$thirdArgType instanceof Type) { + return new GenericObjectType(DatePeriod::class, [ + $firstArgType, + new NullType(), + new IntegerType(), + ]); + } + + if ((new ObjectType(DateTimeInterface::class))->isSuperTypeOf($thirdArgType)->yes()) { + return new GenericObjectType(DatePeriod::class, [ + $firstArgType, + $thirdArgType, + new NullType(), + ]); + } + + if ($thirdArgType->isInteger()->yes()) { + return new GenericObjectType(DatePeriod::class, [ + $firstArgType, + new NullType(), + $thirdArgType, + ]); + } + + return new ObjectType(DatePeriod::class); + } + +} diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php new file mode 100644 index 0000000000..6bde75bc6d --- /dev/null +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -0,0 +1,66 @@ +getName() === '__construct' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); + } + + 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 DateTime($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/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 new file mode 100644 index 0000000000..ef42505ede --- /dev/null +++ b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php @@ -0,0 +1,51 @@ +getName(), ['date_create_from_format', 'date_create_immutable_from_format'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $formats = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + $datetimes = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); + + if (count($formats) === 0 || count($datetimes) === 0) { + return null; + } + + $types = []; + $className = $functionReflection->getName() === 'date_create_from_format' ? DateTime::class : DateTimeImmutable::class; + 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 95355628cb..9d4ac3d682 100644 --- a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -11,8 +12,9 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function count; -class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; @@ -25,22 +27,22 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'define' && $context->null() - && count($node->args) >= 2; + && count($node->getArgs()) >= 2; } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $constantName = $scope->getType($node->args[0]->value); + $constantName = $scope->getType($node->getArgs()[0]->value); if ( !$constantName instanceof ConstantStringType || $constantName->getValue() === '' @@ -49,12 +51,13 @@ public function specifyTypes( } return $this->typeSpecifier->create( - new \PhpParser\Node\Expr\ConstFetch( - new \PhpParser\Node\Name\FullyQualified($constantName->getValue()) + new Node\Expr\ConstFetch( + new Node\Name\FullyQualified($constantName->getValue()), ), - $scope->getType($node->args[1]->value), - TypeSpecifierContext::createTruthy() - ); + $scope->getType($node->getArgs()[1]->value), + TypeSpecifierContext::createTruthy(), + $scope, + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 75a01e0f28..01c310459b 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -12,12 +12,17 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; 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; @@ -26,22 +31,22 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'defined' - && count($node->args) >= 1 - && !$context->null(); + && count($node->getArgs()) >= 1 + && $context->true(); } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $constantName = $scope->getType($node->args[0]->value); + $constantName = $scope->getType($node->getArgs()[0]->value); if ( !$constantName instanceof ConstantStringType || $constantName->getValue() === '' @@ -49,12 +54,16 @@ public function specifyTypes( return new SpecifiedTypes([], []); } + $expr = $this->constantHelper->createExprFromConstantName($constantName->getValue()); + if ($expr === null) { + return new SpecifiedTypes([], []); + } + return $this->typeSpecifier->create( - new \PhpParser\Node\Expr\ConstFetch( - new \PhpParser\Node\Name\FullyQualified($constantName->getValue()) - ), + $expr, new MixedType(), - $context + $context, + $scope, ); } diff --git a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php index a134c6d4a2..5de459fb73 100644 --- a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php @@ -7,11 +7,12 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class DioStatDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +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 cd98bc0967..50ecba6226 100644 --- a/src/Type/Php/DsMapDynamicReturnTypeExtension.php +++ b/src/Type/Php/DsMapDynamicReturnTypeExtension.php @@ -5,12 +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\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use PHPStan\Type\TypeWithClassName; +use function count; +use function in_array; final class DsMapDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -22,39 +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 = $methodReflection->getVariants()[0]->getReturnType(); - - if (count($methodCall->args) > 1) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->args, - $methodReflection->getVariants() - )->getReturnType(); + $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 { - return !$type instanceof TemplateType; - } - ) - ); - - if (count($types) === 1) { - return $types[0]; - } - - return TypeCombinator::union(...$types); + if ($argsCount === 0) { + return null; } - return $returnType; + $mapType = $scope->getType($methodCall->var); + if (!$mapType instanceof TypeWithClassName) { + return null; + } + + $mapAncestor = $mapType->getAncestorWithClassName('Ds\Map'); + if ($mapAncestor === null) { + return null; + } + + $mapAncestorClass = $mapAncestor->getClassReflection(); + if ($mapAncestorClass === null) { + return null; + } + + $valueType = $mapAncestorClass->getActiveTemplateTypeMap()->getType('TValue'); + if ($valueType === null) { + return null; + } + + return $valueType; } } diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index fdead7b788..d43c328df5 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -4,20 +4,34 @@ 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\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; use PHPStan\Type\Constant\ConstantStringType; +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; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ExplodeFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'explode'; @@ -26,24 +40,52 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->args) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - $delimiterType = $scope->getType($functionCall->args[0]->value); - $isSuperset = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); - if ($isSuperset->yes()) { + $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()) { - return new ArrayType(new IntegerType(), new StringType()); } - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $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()); + } + + if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) { + $returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false)); + } + 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 8ba205dfd3..26c2773555 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -2,194 +2,37 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -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\MixedType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; +use function count; +use function strtolower; -class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension +final class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private ConstantStringType $flagsString; - - /** @var array */ - private array $filterTypeMap; - - public function __construct() + public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) { - if (!defined('FILTER_SANITIZE_EMAIL')) { - return; - } - - $booleanType = new BooleanType(); - $floatType = new FloatType(); - $intType = new IntegerType(); - $stringType = new StringType(); - - $this->filterTypeMap = [ - FILTER_UNSAFE_RAW => $stringType, - FILTER_SANITIZE_EMAIL => $stringType, - FILTER_SANITIZE_ENCODED => $stringType, - FILTER_SANITIZE_NUMBER_FLOAT => $stringType, - FILTER_SANITIZE_NUMBER_INT => $stringType, - FILTER_SANITIZE_SPECIAL_CHARS => $stringType, - FILTER_SANITIZE_STRING => $stringType, - FILTER_SANITIZE_URL => $stringType, - FILTER_VALIDATE_BOOLEAN => $booleanType, - FILTER_VALIDATE_EMAIL => $stringType, - FILTER_VALIDATE_FLOAT => $floatType, - FILTER_VALIDATE_INT => $intType, - FILTER_VALIDATE_IP => $stringType, - FILTER_VALIDATE_MAC => $stringType, - FILTER_VALIDATE_REGEXP => $stringType, - FILTER_VALIDATE_URL => $stringType, - ]; - - if (defined('FILTER_SANITIZE_MAGIC_QUOTES')) { - $this->filterTypeMap[FILTER_SANITIZE_MAGIC_QUOTES] = $stringType; - } - - if (defined('FILTER_SANITIZE_ADD_SLASHES')) { - $this->filterTypeMap[FILTER_SANITIZE_ADD_SLASHES] = $stringType; - } - - $this->flagsString = new ConstantStringType('flags'); } public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return defined('FILTER_SANITIZE_EMAIL') && strtolower($functionReflection->getName()) === 'filter_var'; - } - - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope - ): Type - { - $mixedType = new MixedType(); - - $filterArg = $functionCall->args[1] ?? null; - if ($filterArg === null) { - $filterValue = FILTER_DEFAULT; - } else { - $filterType = $scope->getType($filterArg->value); - if (!$filterType instanceof ConstantIntegerType) { - return $mixedType; - } - $filterValue = $filterType->getValue(); - } - - $flagsArg = $functionCall->args[2] ?? null; - $inputType = $scope->getType($functionCall->args[0]->value); - $exactType = $this->determineExactType($inputType, $filterValue); - if ($exactType !== null) { - $type = $exactType; - } else { - $type = $this->filterTypeMap[$filterValue] ?? $mixedType; - $otherType = $this->getOtherType($flagsArg, $scope); - - if ($otherType->isSuperTypeOf($type)->no()) { - $type = new UnionType([$type, $otherType]); - } - } - - if ($this->hasFlag(FILTER_FORCE_ARRAY, $flagsArg, $scope)) { - return new ArrayType(new MixedType(), $type); - } - - return $type; - } - - - private function determineExactType(Type $in, int $filterValue): ?Type - { - if (($filterValue === FILTER_VALIDATE_BOOLEAN && $in instanceof BooleanType) - || ($filterValue === FILTER_VALIDATE_INT && $in instanceof IntegerType) - || ($filterValue === FILTER_VALIDATE_FLOAT && $in instanceof FloatType)) { - return $in; - } - - if ($filterValue === 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(FILTER_NULL_ON_FAILURE, $flagsArg, $scope)) { - return new NullType(); - } - - return $falseType; + return strtolower($functionReflection->getName()) === 'filter_var'; } - private function getDefault(Node\Arg $expression, Scope $scope): ?Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $exprType = $scope->getType($expression->value); - if (!$exprType instanceof ConstantArrayType) { - return null; - } - - $optionsType = $exprType->getOffsetValueType(new ConstantStringType('options')); - if (!$optionsType instanceof ConstantArrayType) { + if (count($functionCall->getArgs()) < 1) { 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; - } + $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 $exprType->getOffsetValueType($this->flagsString); + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); } } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..5d320104a7 --- /dev/null +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -0,0 +1,62 @@ +getName() === 'function_exists' && 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); + if ($argType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('function_exists'), [ + new Arg(new String_(ltrim($argType->getValue(), '\\'))), + ]), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + new CallableType(), + $context, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} 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 5a1bc77551..bd1f73b5c2 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -9,18 +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 @@ -30,8 +33,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $args = $functionCall->args; + $args = $functionCall->getArgs(); + if (count($args) === 0) { + if ($scope->isInTrait()) { + return new ClassStringType(); + } + if ($scope->isInClass()) { return new ConstantStringType($scope->getClassReflection()->getName(), true); } @@ -41,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 { @@ -48,20 +60,35 @@ static function (Type $type, callable $traverse): Type { return $traverse($type); } - if ($type instanceof TemplateType && !$type instanceof TypeWithClassName) { - return new GenericClassStringType($type); + 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); + } + + return new UnionType([ + new GenericClassStringType($type), + new ConstantBooleanType(false), + ]); } elseif ($type instanceof MixedType) { - return new ClassStringType(); + return new UnionType([ + new ClassStringType(), + new ConstantBooleanType(false), + ]); } elseif ($type instanceof StaticType) { return new GenericClassStringType($type->getStaticObjectType()); - } elseif ($type instanceof TypeWithClassName) { + } elseif ($objectClassNames !== []) { return new GenericClassStringType($type); } elseif ($type instanceof ObjectWithoutClassType) { return new ClassStringType(); } return new ConstantBooleanType(false); - } + }, ); } 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 b834633536..b03a68655d 100644 --- a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php @@ -6,26 +6,27 @@ 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; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function array_map; +use function count; -class GetParentClassDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class GetParentClassDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct(\PHPStan\Reflection\ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isFunctionSupported( - FunctionReflection $functionReflection + FunctionReflection $functionReflection, ): bool { return $functionReflection->getName() === 'get_parent_class'; @@ -34,45 +35,38 @@ public function isFunctionSupported( public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants() - )->getReturnType(); - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { if ($scope->isInTrait()) { - return $defaultReturnType; + return null; } if ($scope->isInClass()) { return $this->findParentClassType( - $scope->getClassReflection() + $scope->getClassReflection(), ); } return new ConstantBooleanType(false); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { - return $defaultReturnType; + return null; } - $constantStrings = TypeUtils::getConstantStrings($argType); + $constantStrings = $argType->getConstantStrings(); if (count($constantStrings) > 0) { - return \PHPStan\Type\TypeCombinator::union(...array_map(function (ConstantStringType $stringType): Type { - return $this->findParentClassNameType($stringType->getValue()); - }, $constantStrings)); + return TypeCombinator::union(...array_map(fn (ConstantStringType $stringType): Type => $this->findParentClassNameType($stringType->getValue()), $constantStrings)); } - $classNames = TypeUtils::getDirectClassNames($argType); + $classNames = $argType->getObjectClassNames(); if (count($classNames) > 0) { - return \PHPStan\Type\TypeCombinator::union(...array_map(function (string $classNames): Type { - return $this->findParentClassNameType($classNames); - }, $classNames)); + return TypeCombinator::union(...array_map(fn (string $classNames): Type => $this->findParentClassNameType($classNames), $classNames)); } - return $defaultReturnType; + return null; } private function findParentClassNameType(string $className): Type @@ -84,15 +78,23 @@ 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( - ClassReflection $classReflection + ClassReflection $classReflection, ): Type { $parentClass = $classReflection->getParentClass(); - if ($parentClass === false) { + if ($parentClass === null) { return new ConstantBooleanType(false); } diff --git a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php index 8fdb910688..8b6a6133cf 100644 --- a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php @@ -7,15 +7,15 @@ 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; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class GettimeofdayDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -38,13 +38,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]); $floatType = new FloatType(); - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return $arrayType; } - $argType = $scope->getType($functionCall->args[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $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 new file mode 100644 index 0000000000..85ba080957 --- /dev/null +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -0,0 +1,157 @@ + [ + '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, + ], + ]; + + private const NON_CRYPTOGRAPHIC_ALGORITHMS = [ + 'adler32', + 'crc32', + 'crc32b', + 'crc32c', + 'fnv132', + 'fnv1a32', + 'fnv164', + 'fnv1a64', + 'joaat', + 'murmur3a', + 'murmur3c', + 'murmur3f', + 'xxh32', + 'xxh64', + 'xxh3', + 'xxh128', + ]; + + /** @var array */ + private array $hashAlgorithms; + + public function __construct(private PhpVersion $phpVersion) + { + $this->hashAlgorithms = hash_algos(); + } + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + $name = strtolower($functionReflection->getName()); + return isset(self::SUPPORTED_FUNCTIONS[$name]); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $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); + + $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))); + } + + return $stringReturnType; + } + + $neverType = new NeverType(); + $falseType = new ConstantBooleanType(false); + $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $falseType; + + $returnTypes = array_map( + function (ConstantStringType $type) use ($functionData, $stringReturnType, $invalidAlgorithmType) { + $algorithm = strtolower($type->getValue()); + if (!in_array($algorithm, $this->hashAlgorithms, true)) { + return $invalidAlgorithmType; + } + if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { + return $invalidAlgorithmType; + } + return $stringReturnType; + }, + $constantAlgorithmTypes, + ); + + $returnType = TypeCombinator::union(...$returnTypes); + + if ($functionData['possiblyFalse'] && !$neverType->isSuperTypeOf($returnType)->yes()) { + $returnType = TypeCombinator::union($returnType, $falseType); + } + + return $returnType; + } + +} diff --git a/src/Type/Php/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 37b255b598..fdbf783378 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -5,15 +5,18 @@ 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; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; +use function count; -class HrtimeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,16 +26,16 @@ 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); - $numberType = TypeCombinator::union(new IntegerType(), new FloatType()); + $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->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return $arrayType; } - $argType = $scope->getType($functionCall->args[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $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 new file mode 100644 index 0000000000..15d1d86706 --- /dev/null +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -0,0 +1,146 @@ +getName(), [ + 'implode', + 'join', + ], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) === 1) { + $argType = $scope->getType($args[0]->value); + if ($argType->isArray()->yes()) { + return $this->implode($argType, new ConstantStringType('')); + } + } + + if (count($args) !== 2) { + return new StringType(); + } + + $separatorType = $scope->getType($args[0]->value); + $arrayType = $scope->getType($args[1]->value); + + return $this->implode($arrayType, $separatorType); + } + + private function implode(Type $arrayType, Type $separatorType): Type + { + if (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 ($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(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + + private function inferConstantType(ConstantArrayType $arrayType, ConstantStringType $separatorType): ?Type + { + $strings = []; + foreach ($arrayType->getAllArrays() as $array) { + $valueTypes = $array->getValueTypes(); + + $arrayValues = []; + $combinationsCount = 1; + foreach ($valueTypes as $valueType) { + $constScalars = $valueType->getConstantScalarValues(); + if (count($constScalars) === 0) { + return null; + } + $arrayValues[] = $constScalars; + $combinationsCount *= count($constScalars); + } + + 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 64065fb3d3..d83e9f78ad 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -2,21 +2,29 @@ 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 \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -26,31 +34,138 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'in_array' - && count($node->args) >= 3 && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $strictNodeType = $scope->getType($node->args[2]->value); - if (!(new ConstantBooleanType(true))->isSuperTypeOf($strictNodeType)->yes()) { - return new SpecifiedTypes([], []); + $argsCount = count($node->getArgs()); + if ($argsCount < 2) { + return new SpecifiedTypes(); } - $arrayValueType = $scope->getType($node->args[1]->value)->getIterableValueType(); + $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; + } + } + 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->args[0]->value, + $specifiedTypes = $this->typeSpecifier->create( + $needleExpr, $arrayValueType, - $context + $context, + $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 new file mode 100644 index 0000000000..46920122a6 --- /dev/null +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -0,0 +1,50 @@ +getName() === 'intdiv'; + } + + public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type + { + if (count($funcCall->getArgs()) < 2) { + return $functionReflection->getThrowType(); + } + + $valueType = $scope->getType($funcCall->getArgs()[0]->value)->toInteger(); + $containsMin = $valueType->isSuperTypeOf(new ConstantIntegerType(PHP_INT_MIN)); + + $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); + } + } + + $divisionByZero = $divisorType->isSuperTypeOf(new ConstantIntegerType(0)); + if (!$divisionByZero->no()) { + return new ObjectType(DivisionByZeroError::class); + } + + return null; + } + +} diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 6018f3f746..bc4591b9a8 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -2,9 +2,7 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -14,62 +12,54 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StaticType; -use PHPStan\Type\StringType; +use function count; +use function strtolower; -class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; + + public function __construct( + private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_a' - && isset($node->args[0]) - && isset($node->args[1]) && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + if (count($node->getArgs()) < 2) { + return new SpecifiedTypes(); } + $classType = $scope->getType($node->getArgs()[1]->value); - $classNameArgExpr = $node->args[1]->value; - $classNameArgExprType = $scope->getType($classNameArgExpr); - if ( - $classNameArgExpr instanceof ClassConstFetch - && $classNameArgExpr->class instanceof Name - && $classNameArgExpr->name instanceof \PhpParser\Node\Identifier - && strtolower($classNameArgExpr->name->name) === 'class' - ) { - $className = $scope->resolveName($classNameArgExpr->class); - if (strtolower($classNameArgExpr->class->toString()) === 'static') { - $objectType = new StaticType($className); - } else { - $objectType = new ObjectType($className); - } - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context); - } elseif ($classNameArgExprType instanceof ConstantStringType) { - $objectType = new ObjectType($classNameArgExprType->getValue()); - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context); - } elseif ($context->true()) { - $objectType = new ObjectWithoutClassType(); - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context); - } else { - $types = new SpecifiedTypes(); + if (!$classType instanceof ConstantStringType && !$context->true()) { + return new SpecifiedTypes([], []); } - if (isset($node->args[2]) && $context->true()) { - if (!$scope->getType($node->args[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) { - $types = $types->intersectWith($this->typeSpecifier->create($node->args[0]->value, new StringType(), $context)); - } + $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)); + + $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true); + + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + return new SpecifiedTypes([], []); } - return $types; + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + $resultType, + $context, + $scope, + ); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php new file mode 100644 index 0000000000..d1f4a1bd82 --- /dev/null +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -0,0 +1,78 @@ +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 ($objectOrClassTypeClassNames, $allowString, $allowSameClass): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + if ($type instanceof ConstantStringType) { + if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) { + return new NeverType(); + } + if ($allowString) { + return TypeCombinator::union( + new ObjectType($type->getValue()), + new GenericClassStringType(new ObjectType($type->getValue())), + ); + } + + return new ObjectType($type->getValue()); + } + if ($type instanceof GenericClassStringType) { + if ($allowString) { + return TypeCombinator::union( + $type->getGenericType(), + $type, + ); + } + + return $type->getGenericType(); + } + if ($allowString) { + return TypeCombinator::union( + new ObjectWithoutClassType(), + new ClassStringType(), + ); + } + + return new ObjectWithoutClassType(); + }, + ); + } + +} diff --git a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php index 5a9fedc8d8..e31033185e 100644 --- a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php @@ -9,29 +9,33 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; +use function strtolower; -class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_array' - && isset($node->args[0]) && !$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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->args[0]->value, new ArrayType(new MixedType(), new MixedType()), $context); + 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 0c67c970ca..0000000000 --- a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,41 +0,0 @@ -getName()) === 'is_bool' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new BooleanType(), $context); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php index b79b78988b..42c5fe8505 100644 --- a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php @@ -12,41 +12,43 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\CallableType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function count; +use function strtolower; -class IsCallableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsCallableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Type\Php\MethodExistsTypeSpecifyingExtension $methodExistsExtension; + private TypeSpecifier $typeSpecifier; - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - public function __construct(MethodExistsTypeSpecifyingExtension $methodExistsExtension) + public function __construct(private MethodExistsTypeSpecifyingExtension $methodExistsExtension) { - $this->methodExistsExtension = $methodExistsExtension; } public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_callable' - && isset($node->args[0]) && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + if (!isset($node->getArgs()[0])) { + return new SpecifiedTypes(); } - $value = $node->args[0]->value; + $value = $node->getArgs()[0]->value; $valueType = $scope->getType($value); if ( $value instanceof Array_ && count($value->items) === 2 - && $valueType instanceof ConstantArrayType + && $valueType->isConstantArray()->yes() && !$valueType->isCallable()->no() ) { $functionCall = new FuncCall(new Name('method_exists'), [ @@ -56,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); + 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 4b42c769d5..0000000000 --- a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,51 +0,0 @@ -getName()) === 'is_countable' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create( - $node->args[0]->value, - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(\Countable::class), - ]), - $context - ); - } - - 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 9f6e4ff893..0000000000 --- a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,45 +0,0 @@ -getName()), [ - 'is_float', - 'is_double', - 'is_real', - ], true) - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new FloatType(), $context); - } - - 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 c4b2382307..0000000000 --- a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,45 +0,0 @@ -getName()), [ - 'is_int', - 'is_integer', - 'is_long', - ], true) - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new IntegerType(), $context); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php index 3a6f5044de..a8404ef99f 100644 --- a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php @@ -9,29 +9,34 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; +use function strtolower; -class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_iterable' - && isset($node->args[0]) && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->args[0]->value, new IterableType(new MixedType(), new MixedType()), $context); + if (!isset($node->getArgs()[0])) { + return new SpecifiedTypes(); + } + + 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 5b86a53f5f..0000000000 --- a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,41 +0,0 @@ -getName()) === 'is_null' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new NullType(), $context); - } - - 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 ddd8331b5f..0000000000 --- a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,59 +0,0 @@ -getName() === 'is_numeric' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $argType = $scope->getType($node->args[0]->value); - if ($context->truthy() && !(new StringType())->isSuperTypeOf($argType)->no() && !$argType instanceof MixedType) { - return new SpecifiedTypes([], []); - } - - $numericTypes = [ - new IntegerType(), - new FloatType(), - ]; - - if ($context->truthy()) { - $numericTypes[] = new StringType(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new UnionType($numericTypes), $context); - } - - 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 2106d1dfef..0000000000 --- a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,41 +0,0 @@ -getName()) === 'is_object' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new ObjectWithoutClassType(), $context); - } - - 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 8033ba1890..0000000000 --- a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,41 +0,0 @@ -getName()) === 'is_resource' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new ResourceType(), $context); - } - - 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 fea9edba36..0000000000 --- a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,50 +0,0 @@ -getName() === 'is_scalar' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new UnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - new BooleanType(), - ]), $context); - } - - 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 3bceb79afc..0000000000 --- a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,41 +0,0 @@ -getName()) === 'is_string' - && isset($node->args[0]) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->args[0]->value, new StringType(), $context); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index 7195a34210..c62a11b150 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -9,96 +9,57 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\UnionType; +use function count; +use function strtolower; -class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension +final class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; + + public function __construct( + private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_subclass_of' - && count($node->args) >= 2 && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $objectType = $scope->getType($node->args[0]->value); - $classType = $scope->getType($node->args[1]->value); - $allowStringType = isset($node->args[2]) ? $scope->getType($node->args[2]->value) : new ConstantBooleanType(true); - $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - - if (!$classType instanceof ConstantStringType) { - if ($context->truthy()) { - if ($allowString) { - $type = TypeCombinator::union( - new ObjectWithoutClassType(), - new ClassStringType() - ); - } else { - $type = new ObjectWithoutClassType(); - } + if (!$context->true() || count($node->getArgs()) < 2) { + return new SpecifiedTypes(); + } - return $this->typeSpecifier->create( - $node->args[0]->value, - $type, - $context - ); - } + $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)); - return new SpecifiedTypes(); + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($objectOrClassType instanceof GenericClassStringType && $classType instanceof GenericClassStringType) { + return new SpecifiedTypes([], []); } - $type = TypeTraverser::map($objectType, static function (Type $type, callable $traverse) use ($classType, $allowString): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - if ($type instanceof IntersectionType) { - return $traverse($type); - } - if ($allowString) { - if ($type instanceof StringType) { - return new GenericClassStringType(new ObjectType($classType->getValue())); - } - } - if ($type instanceof ObjectWithoutClassType || $type instanceof TypeWithClassName) { - return new ObjectType($classType->getValue()); - } - if ($type instanceof MixedType) { - $objectType = new ObjectType($classType->getValue()); - if ($allowString) { - return TypeCombinator::union( - new GenericClassStringType($objectType), - $objectType - ); - } + $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false); - return $objectType; - } - return new NeverType(); - }); + // prevent false-positives in IsAFunctionTypeSpecifyingHelper + if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + return new SpecifiedTypes([], []); + } return $this->typeSpecifier->create( - $node->args[0]->value, - $type, - $context + $node->getArgs()[0]->value, + $resultType, + $context, + $scope, ); } diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..618fdb1fdc --- /dev/null +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -0,0 +1,59 @@ +getName()) === 'iterator_to_array'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $arguments = $functionCall->getArgs(); + + if ($arguments === []) { + return null; + } + + $traversableType = $scope->getType($arguments[0]->value); + + if (isset($arguments[1])) { + $preserveKeysType = $scope->getType($arguments[1]->value); + + 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 7acb07ef21..65d82ccd7b 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -2,21 +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 is_bool; +use function json_decode; -class JsonThrowOnErrorDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var array */ @@ -25,76 +29,103 @@ class JsonThrowOnErrorDynamicReturnTypeExtension implements \PHPStan\Type\Dynami 'json_decode' => 3, ]; - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private BitwiseFlagHelper $bitwiseFlagAnalyser, + ) { - $this->reflectionProvider = $reflectionProvider; } public function isFunctionSupported( - FunctionReflection $functionReflection + 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( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->args[$argumentPosition])) { + $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->args[$argumentPosition]->value; - $constrictedReturnType = TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); - if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr)) { - return $constrictedReturnType; + $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; + 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): bool + /** + * Is "json_decode(..., true)"? + */ + private function isForceArray(FuncCall $funcCall, Scope $scope): bool { - if ($expr instanceof ConstFetch && $expr->name->toCodeString() === '\JSON_THROW_ON_ERROR') { - return true; + $args = $funcCall->getArgs(); + if (!isset($args[1])) { + return false; } - if (!$expr instanceof BitwiseOr) { + $secondArgType = $scope->getType($args[1]->value); + $secondArgValue = $secondArgType instanceof ConstantScalarType ? $secondArgType->getValue() : null; + + if (is_bool($secondArgValue)) { + return $secondArgValue; + } + + if ($secondArgValue !== null || !isset($args[3])) { return false; } - return $this->isBitwiseOrWithJsonThrowOnError($expr->left) || - $this->isBitwiseOrWithJsonThrowOnError($expr->right); + // 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 new file mode 100644 index 0000000000..68e7855bb7 --- /dev/null +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -0,0 +1,64 @@ + 1, + 'json_decode' => 3, + ]; + + public function __construct( + private ReflectionProvider $reflectionProvider, + private BitwiseFlagHelper $bitwiseFlagAnalyser, + ) + { + } + + public function isFunctionSupported( + FunctionReflection $functionReflection, + ): bool + { + return in_array( + $functionReflection->getName(), + [ + 'json_encode', + 'json_decode', + ], + true, + ) && $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null); + } + + public function getThrowTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $argumentPosition = self::ARGUMENTS_POSITIONS[$functionReflection->getName()]; + if (!isset($functionCall->getArgs()[$argumentPosition])) { + return null; + } + + $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; + if (!$this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->no()) { + return new ObjectType('JsonException'); + } + + 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 1369e545d6..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 @@ -24,17 +22,16 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->args[0])) { - return $defaultReturnType; + if (!isset($functionCall->getArgs()[0])) { + return null; } - $argType = $scope->getType($functionCall->args[0]->value); - $isString = (new StringType())->isSuperTypeOf($argType); - $isArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($argType); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $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 0b98c19e43..dc27fe349b 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -4,22 +4,27 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +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\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function array_key_exists; +use function array_map; +use function array_unique; +use function count; -class MbFunctionsReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $supportedEncodings; + use MbFunctionsReturnTypeExtensionTrait; /** @var int[] */ private array $encodingPositionMap = [ @@ -27,24 +32,12 @@ class MbFunctionsReturnTypeExtension implements \PHPStan\Type\DynamicFunctionRet 'mb_regex_encoding' => 1, 'mb_internal_encoding' => 1, 'mb_encoding_aliases' => 1, - 'mb_strlen' => 2, 'mb_chr' => 2, 'mb_ord' => 2, ]; - public function __construct() + public function __construct(private PhpVersion $phpVersion) { - $supportedEncodings = []; - if (function_exists('mb_list_encodings')) { - foreach (mb_list_encodings() as $encoding) { - $aliases = mb_encoding_aliases($encoding); - if ($aliases === false) { - throw new \PHPStan\ShouldNotHappenException(); - } - $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); - } - } - $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -52,33 +45,35 @@ 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->args) < $positionEncodingParam) { + if (count($functionCall->getArgs()) < $positionEncodingParam) { return TypeCombinator::remove($returnType, new BooleanType()); } - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[$positionEncodingParam - 1]->value)); - $results = array_unique(array_map(function (ConstantStringType $encoding): bool { - return $this->isSupportedEncoding($encoding->getValue()); - }, $strings)); + $strings = $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()]))) { return count($results) === 1 ? new ConstantBooleanType($results[0]) : new BooleanType(); } if (count($results) === 1) { + $invalidEncodingReturn = new ConstantBooleanType(false); + if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { + $invalidEncodingReturn = new NeverType(); + } + return $results[0] ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) - : new ConstantBooleanType(false); + : $invalidEncodingReturn; } return $returnType; diff --git a/src/Type/Php/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 new file mode 100644 index 0000000000..a782db6686 --- /dev/null +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -0,0 +1,135 @@ +getName() === 'mb_substitute_character'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $minCodePoint = $this->phpVersion->getVersionId() < 80000 ? 1 : 0; + $maxCodePoint = $this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter() ? 0x10FFFF : 0xFFFE; + $ranges = []; + + if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { + // Surrogates aren't valid in PHP 7.2+ + $ranges[] = IntegerRangeType::fromInterval($minCodePoint, 0xD7FF); + $ranges[] = IntegerRangeType::fromInterval(0xE000, $maxCodePoint); + } else { + $ranges[] = IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint); + } + + if (!isset($functionCall->getArgs()[0])) { + return TypeCombinator::union( + new ConstantStringType('none'), + new ConstantStringType('long'), + new ConstantStringType('entity'), + ...$ranges, + ); + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $isString = $argType->isString(); + $isNull = $argType->isNull(); + $isInteger = $argType->isInteger(); + + if ($isString->no() && $isNull->no() && $isInteger->no()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new BooleanType(); + } + + if ($isInteger->yes()) { + $invalidRanges = []; + + foreach ($ranges as $range) { + $isInRange = $range->isSuperTypeOf($argType); + + if ($isInRange->yes()) { + return new ConstantBooleanType(true); + } + + $invalidRanges[] = $isInRange->no(); + } + + if ($argType instanceof ConstantIntegerType || !in_array(false, $invalidRanges, true)) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); + } + } elseif ($isString->yes()) { + if ($argType->isNonEmptyString()->no()) { + // The empty string was a valid alias for "none" in PHP < 8. + if ($this->phpVersion->isEmptyStringValidAliasForNoneInMbSubstituteCharacter()) { + return new ConstantBooleanType(true); + } + + return new NeverType(); + } + + if (!$this->phpVersion->isNumericStringValidArgInMbSubstituteCharacter() && $argType->isNumericString()->yes()) { + return new NeverType(); + } + + if ($argType instanceof ConstantStringType) { + $value = strtolower($argType->getValue()); + + if (in_array($value, ['none', 'long', 'entity'], true)) { + return new ConstantBooleanType(true); + } + + if ($argType->isNumericString()->yes()) { + $codePoint = (int) $value; + $isValid = $codePoint >= $minCodePoint && $codePoint <= $maxCodePoint; + + if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { + $isValid = $isValid && ($codePoint < 0xD800 || $codePoint > 0xDFFF); + } + + return new ConstantBooleanType($isValid); + } + + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); + } + } elseif ($isNull->yes()) { + // The $substitute_character arg is nullable in PHP 8+ + return new ConstantBooleanType($this->phpVersion->isNullValidArgInMbSubstituteCharacter()); + } + + return new BooleanType(); + } + +} diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 533c0e91ff..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; @@ -10,14 +11,16 @@ use PHPStan\Analyser\TypeSpecifierContext; 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; @@ -30,40 +33,59 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'method_exists' - && $context->truthy() - && count($node->args) >= 2; + && $context->true() + && count($node->getArgs()) >= 2; } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $objectType = $scope->getType($node->args[0]->value); - if (!$objectType instanceof ObjectType) { - if ((new StringType())->isSuperTypeOf($objectType)->yes()) { - return new 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, + ); } - $methodNameType = $scope->getType($node->args[1]->value); - if (!$methodNameType instanceof ConstantStringType) { + $objectType = $scope->getType($node->getArgs()[0]->value); + 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, + ); + } + return new SpecifiedTypes([], []); } return $this->typeSpecifier->create( - $node->args[0]->value, - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType($methodNameType->getValue()), + $node->getArgs()[0]->value, + new UnionType([ + new IntersectionType([ + new ObjectWithoutClassType(), + new HasMethodType($methodNameType->getValue()), + ]), + new ClassStringType(), ]), - $context + $context, + $scope, ); } diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index 15ef4772dd..f0cc1561fb 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -6,14 +6,15 @@ 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; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function count; -class MicrotimeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,13 +24,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return new StringType(); } - $argType = $scope->getType($functionCall->args[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $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 c7284d570b..9faa3563c7 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -2,62 +2,88 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Smaller; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\Ternary; use PHPStan\Analyser\Scope; +use PHPStan\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 \PHPStan\Type\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->args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (!isset($functionCall->getArgs()[0])) { + return null; } - if (count($functionCall->args) === 1) { - $argType = $scope->getType($functionCall->args[0]->value); + if (count($functionCall->getArgs()) === 1) { + $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType->isArray()->yes()) { - $iterableValueType = $argType->getIterableValueType(); - $argumentTypes = []; - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $argumentTypes[] = $innerType; - } - } else { - $argumentTypes[] = $iterableValueType; - } - - return $this->processType( + return $this->processArrayType( $functionReflection->getName(), - $argumentTypes + $argType, ); } return new ErrorType(); } + // rewrite min($x, $y) as $x < $y ? $x : $y + // we don't handle arrays, which have different semantics + $functionName = $functionReflection->getName(); + $args = $functionCall->getArgs(); + if (count($functionCall->getArgs()) === 2) { + $argType0 = $scope->getType($args[0]->value); + $argType1 = $scope->getType($args[1]->value); + + if ($argType0->isArray()->no() && $argType1->isArray()->no()) { + $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( + $comparisonExpr, + $args[0]->value, + $args[1]->value, + )); + } elseif ($functionName === 'max') { + return $scope->getType(new Ternary( + $comparisonExpr, + $args[1]->value, + $args[0]->value, + )); + } + } + } + $argumentTypes = []; - foreach ($functionCall->args as $arg) { + foreach ($functionCall->getArgs() as $arg) { $argType = $scope->getType($arg->value); if ($arg->unpack) { $iterableValueType = $argType->getIterableValueType(); @@ -75,33 +101,72 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } return $this->processType( - $functionReflection->getName(), - $argumentTypes + $functionName, + $argumentTypes, ); } + 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 string $functionName - * @param \PHPStan\Type\Type[] $types - * @return Type + * @param Type[] $types */ private function processType( string $functionName, - array $types + array $types, ): Type { $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; @@ -122,19 +187,19 @@ private function processType( private function compareTypes( Type $firstType, - Type $secondType + Type $secondType, ): ?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; } @@ -143,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 new file mode 100644 index 0000000000..893627aae2 --- /dev/null +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -0,0 +1,95 @@ +getName(), [ + 'addslashes', + 'addcslashes', + 'escapeshellarg', + 'escapeshellcmd', + 'htmlspecialchars', + 'htmlentities', + 'urlencode', + 'urldecode', + 'preg_quote', + 'rawurlencode', + 'rawurldecode', + ], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + 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(), + new AccessoryNonEmptyStringType(), + ]); + } + + 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/NumberFormatFunctionDynamicReturnTypeExtension.php b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..1a564b358c --- /dev/null +++ b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName() === 'number_format'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $stringType = new StringType(); + if (!isset($functionCall->getArgs()[3])) { + return $stringType; + } + + $thousandsType = $scope->getType($functionCall->getArgs()[3]->value); + $decimalType = $scope->getType($functionCall->getArgs()[2]->value); + + if (!$thousandsType instanceof ConstantStringType || $thousandsType->getValue() !== '') { + return $stringType; + } + + if (!$decimalType instanceof ConstantScalarType || !in_array($decimalType->getValue(), [null, '.', ''], true)) { + return $stringType; + } + + return new IntersectionType([ + $stringType, + new AccessoryNumericStringType(), + ]); + } + +} 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 7df7d7d6b4..19d6c20b26 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -5,18 +5,30 @@ 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; use PHPStan\Type\TypeCombinator; +use ValueError; +use function count; +use function parse_url; +use const PHP_URL_FRAGMENT; +use const PHP_URL_HOST; +use const PHP_URL_PASS; +use const PHP_URL_PATH; +use const PHP_URL_PORT; +use const PHP_URL_QUERY; +use const PHP_URL_SCHEME; +use const PHP_URL_USER; final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -27,78 +39,131 @@ 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 { - $defaultReturnType = ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants() - )->getReturnType(); - - if (count($functionCall->args) < 1) { - return $defaultReturnType; + if (count($functionCall->getArgs()) < 1) { + return null; } $this->cacheReturnTypes(); - $urlType = $scope->getType($functionCall->args[0]->value); - if (count($functionCall->args) > 1) { - $componentType = $scope->getType($functionCall->args[1]->value); + $urlType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($functionCall->getArgs()) > 1) { + $componentType = $scope->getType($functionCall->getArgs()[1]->value); - if (!$componentType instanceof ConstantType) { - return $this->createAllComponentsReturnType(); + if (!$componentType->isConstantValue()->yes()) { + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } $componentType = $componentType->toInteger(); - if (!$componentType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } } else { $componentType = new ConstantIntegerType(-1); } - if ($urlType instanceof ConstantStringType) { - $result = @parse_url(http://github.com/$urlType-%3EgetValue(), $componentType->getValue()); + 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } 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 @@ -108,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 dbb09f1a8a..60fac48358 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -2,18 +2,28 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\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 \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'pathinfo'; @@ -21,28 +31,68 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope - ): Type + Node\Expr\FuncCall $functionCall, + Scope $scope, + ): ?Type { - $argsCount = count($functionCall->args); + $argsCount = count($functionCall->getArgs()); if ($argsCount === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } elseif ($argsCount === 1) { - $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 new file mode 100644 index 0000000000..6be372d38f --- /dev/null +++ b/src/Type/Php/PowFunctionReturnTypeExtension.php @@ -0,0 +1,30 @@ +getName() === 'pow'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + 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 new file mode 100644 index 0000000000..c2c23e4155 --- /dev/null +++ b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php @@ -0,0 +1,51 @@ +getName() === 'preg_filter'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $defaultReturn = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + $argsCount = count($functionCall->getArgs()); + if ($argsCount < 3) { + return $defaultReturn; + } + + $subjectType = $scope->getType($functionCall->getArgs()[2]->value); + + if ($subjectType->isArray()->yes()) { + return new ArrayType(new IntegerType(), new StringType()); + } + if ($subjectType->isString()->yes()) { + return new UnionType([new StringType(), new NullType()]); + } + + return $defaultReturn; + } + +} diff --git a/src/Type/Php/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 new file mode 100644 index 0000000000..d51b5314b0 --- /dev/null +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -0,0 +1,52 @@ +getName()) === 'preg_split'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $flagsArg = $functionCall->getArgs()[3] ?? null; + + if ($flagsArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { + $type = new ArrayType( + new IntegerType(), + new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), + ); + return TypeCombinator::union(TypeCombinator::intersect($type, new AccessoryArrayListType()), new ConstantBooleanType(false)); + } + + return null; + } + +} diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index b83f553943..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,21 +14,20 @@ 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 PropertyReflectionFinder $propertyReflectionFinder; - private TypeSpecifier $typeSpecifier; - public function __construct(PropertyReflectionFinder $propertyReflectionFinder) + public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) { - $this->propertyReflectionFinder = $propertyReflectionFinder; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -38,33 +38,42 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'property_exists' - && $context->truthy() - && count($node->args) >= 2; + && $context->true() + && count($node->getArgs()) >= 2; } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $propertyNameType = $scope->getType($node->args[1]->value); + $propertyNameType = $scope->getType($node->getArgs()[1]->value); if (!$propertyNameType instanceof ConstantStringType) { + return $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->args[0]->value); + $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->args[0]->value, - new Identifier($propertyNameType->getValue()) + $node->getArgs()[0]->value, + new Identifier($propertyNameType->getValue()), ); } else { return new SpecifiedTypes([], []); @@ -78,12 +87,13 @@ public function specifyTypes( } return $this->typeSpecifier->create( - $node->args[0]->value, + $node->getArgs()[0]->value, new IntersectionType([ new ObjectWithoutClassType(), new HasPropertyType($propertyNameType->getValue()), ]), - $context + $context, + $scope, ); } diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index 30e9c0f89d..3e0873ee11 100644 --- a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -5,59 +5,77 @@ 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\UnionType; +use function array_map; +use function assert; +use function count; +use function in_array; +use function max; +use function min; -class RandomIntFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'random_int'; + 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 (count($functionCall->args) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (in_array($functionReflection->getName(), ['rand', 'mt_rand'], true) && count($functionCall->getArgs()) === 0) { + return IntegerRangeType::fromInterval(0, null); } - $minType = $scope->getType($functionCall->args[0]->value)->toInteger(); - $maxType = $scope->getType($functionCall->args[1]->value)->toInteger(); + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $minType = $scope->getType($functionCall->getArgs()[0]->value)->toInteger(); + $maxType = $scope->getType($functionCall->getArgs()[1]->value)->toInteger(); return $this->createRange($minType, $maxType); } private function createRange(Type $minType, Type $maxType): Type { - $minValue = array_reduce($minType instanceof UnionType ? $minType->getTypes() : [$minType], static function (int $carry, Type $type): int { - if ($type instanceof IntegerRangeType) { - $value = $type->getMin(); - } elseif ($type instanceof ConstantIntegerType) { - $value = $type->getValue(); - } else { - $value = PHP_INT_MIN; - } - - return min($value, $carry); - }, PHP_INT_MAX); - - $maxValue = array_reduce($maxType instanceof UnionType ? $maxType->getTypes() : [$maxType], static function (int $carry, Type $type): int { - if ($type instanceof IntegerRangeType) { - $value = $type->getMax(); - } elseif ($type instanceof ConstantIntegerType) { - $value = $type->getValue(); - } else { - $value = PHP_INT_MAX; - } - - return max($value, $carry); - }, PHP_INT_MIN); - - return IntegerRangeType::fromInterval($minValue, $maxValue); + $minValues = array_map( + static function (Type $type): ?int { + if ($type instanceof IntegerRangeType) { + return $type->getMin(); + } + if ($type instanceof ConstantIntegerType) { + return $type->getValue(); + } + return null; + }, + $minType instanceof UnionType ? $minType->getTypes() : [$minType], + ); + + $maxValues = array_map( + static function (Type $type): ?int { + if ($type instanceof IntegerRangeType) { + return $type->getMax(); + } + if ($type instanceof ConstantIntegerType) { + return $type->getValue(); + } + return null; + }, + $maxType instanceof UnionType ? $maxType->getTypes() : [$maxType], + ); + + assert(count($minValues) > 0); + assert(count($maxValues) > 0); + + return IntegerRangeType::fromInterval( + in_array(null, $minValues, true) ? null : min($minValues), + in_array(null, $maxValues, true) ? null : max($maxValues), + ); } } diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 65a7ab3236..175370be90 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -5,19 +5,29 @@ 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\ConstantArrayType; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\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 \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const RANGE_LENGTH_THRESHOLD = 50; @@ -27,51 +37,98 @@ 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->args) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) < 2) { + return null; } - $startType = $scope->getType($functionCall->args[0]->value); - $endType = $scope->getType($functionCall->args[1]->value); - $stepType = count($functionCall->args) >= 3 ? $scope->getType($functionCall->args[2]->value) : new ConstantIntegerType(1); + $startType = $scope->getType($functionCall->getArgs()[0]->value); + $endType = $scope->getType($functionCall->getArgs()[1]->value); + $stepType = count($functionCall->getArgs()) >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : new ConstantIntegerType(1); $constantReturnTypes = []; - $startConstants = TypeUtils::getConstantScalars($startType); + $startConstants = $startType->getConstantScalarTypes(); foreach ($startConstants as $startConstant) { - if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType) { + 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) { + 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; } - $rangeLength = (int) ceil(abs($startConstant->getValue() - $endConstant->getValue()) / $stepConstant->getValue()) + 1; - if ($rangeLength > self::RANGE_LENGTH_THRESHOLD) { + try { + $rangeValues = @range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + } catch (ValueError) { continue; } - $keyTypes = []; - $valueTypes = []; + // @phpstan-ignore function.alreadyNarrowedType + if (!is_array($rangeValues)) { + continue; + } - $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); - foreach ($rangeValues as $key => $value) { - $keyTypes[] = new ConstantIntegerType($key); - $valueTypes[] = $scope->getTypeFromValue($value); + if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) { + if ( + $startConstant instanceof ConstantIntegerType + && $endConstant instanceof ConstantIntegerType + && $stepConstant instanceof ConstantIntegerType + ) { + if ($startConstant->getValue() > $endConstant->getValue()) { + $tmp = $startConstant; + $startConstant = $endConstant; + $endConstant = $tmp; + } + return TypeCombinator::intersect( + new ArrayType( + new IntegerType(), + IntegerRangeType::fromInterval($startConstant->getValue(), $endConstant->getValue()), + ), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ); + } + + 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) { + $arrayBuilder->setOffsetValueType(null, $scope->getTypeFromValue($value)); } - $constantReturnTypes[] = new ConstantArrayType($keyTypes, $valueTypes, $rangeLength); + $constantReturnTypes[] = $arrayBuilder->getArray(); } } } @@ -80,27 +137,36 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union(...$constantReturnTypes); } - $startType = TypeUtils::generalizeType($startType); - $endType = TypeUtils::generalizeType($endType); - $stepType = TypeUtils::generalizeType($stepType); + $argType = TypeCombinator::union($startType, $endType); + $isInteger = $argType->isInteger()->yes(); + $isStepInteger = $stepType->isInteger()->yes(); + + if ($isInteger && $isStepInteger) { + 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()); + } + + if ($argType->isFloat()->yes()) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new FloatType()), new AccessoryArrayListType()); + } - if ( - $startType instanceof IntegerType - && $endType instanceof IntegerType - && $stepType instanceof IntegerType - ) { - return new ArrayType(new IntegerType(), new IntegerType()); + $numberType = new UnionType([new IntegerType(), new FloatType()]); + $isNumber = $numberType->isSuperTypeOf($argType)->yes(); + $isNumericString = $argType->isNumericString()->yes(); + if ($isNumber || $isNumericString) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $numberType), new AccessoryArrayListType()); } - if ( - $startType instanceof FloatType - || $endType instanceof FloatType - || $stepType instanceof FloatType - ) { - return new ArrayType(new IntegerType(), new FloatType()); + if ($argType->isString()->yes()) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); } - return new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new FloatType()])); + 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 new file mode 100644 index 0000000000..487857213d --- /dev/null +++ b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php @@ -0,0 +1,42 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionClass::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $classOrString = new UnionType([ + new ClassStringType(), + new ObjectWithoutClassType(), + ]); + if ($classOrString->isSuperTypeOf($valueType)->yes()) { + return null; + } + + return $methodReflection->getThrowType(); + } + +} diff --git a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php new file mode 100644 index 0000000000..f472eab562 --- /dev/null +++ b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php @@ -0,0 +1,71 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return ReflectionClass::class; + } + + public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool + { + return $methodReflection->getName() === 'isSubclassOf' + && isset($node->getArgs()[0]) + && !$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); + $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, + $narrowingType, + $context, + $scope, + )->setAlwaysOverwriteTypes(); + } + +} diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php new file mode 100644 index 0000000000..83df24904f --- /dev/null +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -0,0 +1,55 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionFunction::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 1) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + foreach ($valueType->getConstantStrings() as $constantString) { + if ($constantString->getValue() === '') { + return null; + } + + if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) { + return $methodReflection->getThrowType(); + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php new file mode 100644 index 0000000000..493ec0e9c3 --- /dev/null +++ b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php @@ -0,0 +1,50 @@ +className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->getName() === $this->className + && $methodReflection->getName() === 'getAttributes'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + $argType = $scope->getType($methodCall->getArgs()[0]->value); + $classType = $argType->getClassStringObjectType(); + + 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 new file mode 100644 index 0000000000..c0b0df2cf5 --- /dev/null +++ b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php @@ -0,0 +1,79 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionMethod::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 2) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $propertyType = $scope->getType($methodCall->getArgs()[1]->value); + foreach (TypeUtils::flattenTypes($valueType) as $type) { + if ($type instanceof GenericClassStringType) { + $classes = $type->getGenericType()->getObjectClassNames(); + } elseif ( + $type instanceof ConstantStringType + && $this->reflectionProvider->hasClass($type->getValue()) + ) { + $classes = [$type->getValue()]; + } else { + return $methodReflection->getThrowType(); + } + + foreach ($classes as $class) { + $classReflection = $this->reflectionProvider->getClass($class); + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { + if (!$classReflection->hasMethod($constantPropertyString->getValue())) { + return $methodReflection->getThrowType(); + } + } + } + + $valueType = TypeCombinator::remove($valueType, $type); + } + + if (!$valueType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + // Look for non constantStrings value. + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { + $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); + } + + if (!$propertyType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php new file mode 100644 index 0000000000..b988a7883a --- /dev/null +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -0,0 +1,67 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionProperty::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) < 2) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $propertyType = $scope->getType($methodCall->getArgs()[1]->value); + foreach ($valueType->getConstantStrings() as $constantString) { + if (!$this->reflectionProvider->hasClass($constantString->getValue())) { + return $methodReflection->getThrowType(); + } + + $classReflection = $this->reflectionProvider->getClass($constantString->getValue()); + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { + if (!$classReflection->hasProperty($constantPropertyString->getValue())) { + return $methodReflection->getThrowType(); + } + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + // Look for non constantStrings value. + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { + $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); + } + + if (!$propertyType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} 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 752aa8560f..01bb1dd29e 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -6,43 +6,57 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +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; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; 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 $functions = [ + 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, + ]; + + 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->functions); + return array_key_exists($functionReflection->getName(), self::FUNCTIONS_SUBJECT_POSITION); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $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); } @@ -52,30 +66,89 @@ public function getTypeFromFunctionCall( private function getPreliminarilyResolvedTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - $argumentPosition = $this->functions[$functionReflection->getName()]; - if (count($functionCall->args) <= $argumentPosition) { - 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->args[$argumentPosition]->value); - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($subjectArgumentType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $stringType = new StringType(); - $arrayType = new ArrayType(new MixedType(), new MixedType()); - $isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType); - $isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType); + 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 (count($accessories) > 0) { + $accessories[] = new StringType(); + return new IntersectionType($accessories); + } + } + } + + $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; } @@ -83,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 new file mode 100644 index 0000000000..8d064c1ef3 --- /dev/null +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -0,0 +1,99 @@ +getName(), + [ + 'round', + 'ceil', + 'floor', + ], + true, + ); + } + + 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); + } else { + // PHP 7 returns null with a missing parameter. + $noArgsReturnType = new NullType(); + } + + if (count($functionCall->getArgs()) < 1) { + return $noArgsReturnType; + } + + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + + if ($firstArgType instanceof MixedType) { + return $defaultReturnType; + } + + if ($this->phpVersion->hasStricterRoundFunctions()) { + $allowed = TypeCombinator::union( + new IntegerType(), + new FloatType(), + ); + + if (!$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); + } + } elseif ($firstArgType->isArray()->yes()) { + // PHP 7 returns false if the parameter is an array. + return new ConstantBooleanType(false); + } + + return new FloatType(); + } + +} diff --git a/src/Type/Php/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 562ec373be..a0f33f1841 100644 --- a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php @@ -12,8 +12,9 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use SimpleXMLElement; +use function count; -class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -28,7 +29,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (count($methodCall->args) === 1) { + if (count($methodCall->getArgs()) === 1) { return new BooleanType(); } return new UnionType([new StringType(), new ConstantBooleanType(false)]); diff --git a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php index 0b2feb187b..c670914835 100644 --- a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php +++ b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php @@ -6,20 +6,21 @@ use PHPStan\Reflection\Php\SimpleXMLElementProperty; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; -class SimpleXMLElementClassPropertyReflectionExtension implements PropertiesClassReflectionExtension +final class SimpleXMLElementClassPropertyReflectionExtension implements PropertiesClassReflectionExtension { public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return $classReflection->getName() === 'SimpleXMLElement'; + return $classReflection->is('SimpleXMLElement'); } - public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { - return new SimpleXMLElementProperty($classReflection, new ObjectType('SimpleXMLElement')); + 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 new file mode 100644 index 0000000000..28995db8ea --- /dev/null +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -0,0 +1,59 @@ +getName() === '__construct' + && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + $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); + } + } finally { + libxml_use_internal_errors($internalErrorsOld); + } + + if (!$valueType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php new file mode 100644 index 0000000000..7255d64887 --- /dev/null +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -0,0 +1,57 @@ +getName() === 'xpath'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (!isset($methodCall->getArgs()[0])) { + return null; + } + + $argType = $scope->getType($methodCall->getArgs()[0]->value); + + $xmlElement = new SimpleXMLElement(''); + + 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 null; + } + + $argType = TypeCombinator::remove($argType, $constantString); + } + + if (!$argType instanceof NeverType) { + 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 62016da18f..e379c4cc3c 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -2,56 +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\Type\ConstantScalarType; +use PHPStan\Reflection\InitializerExprTypeResolver; +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\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 + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return null; + } + + $constantType = $this->getConstantType($args, $functionReflection, $scope); + if ($constantType !== null) { + return $constantType; + } + + $formatType = $scope->getType($args[0]->value); + $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 { + 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 = []; - $returnType = new StringType(); - foreach ($functionCall->args as $arg) { + $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(); + } + } } - $values[] = $argType->getValue(); + if (count($constantScalarValues) === 0) { + return null; + } + + $values[] = $constantScalarValues; + $combinationsCount *= count($constantScalarValues); } - if (count($values) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } - $format = array_shift($values); - if (!is_string($format)) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $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; + } } - try { - $value = @sprintf($format, ...$values); - } catch (\Throwable $e) { - return $returnType; + 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(); } - return $scope->getTypeFromValue($value); + 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 ccc97ff5c9..4a0141b106 100644 --- a/src/Type/Php/StatDynamicReturnTypeExtension.php +++ b/src/Type/Php/StatDynamicReturnTypeExtension.php @@ -10,11 +10,15 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use SplFileObject; +use function in_array; -class StatDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension, \PHPStan\Type\DynamicMethodReturnTypeExtension +final class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,12 +28,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - return $this->getReturnType(); + return TypeCombinator::union($this->getReturnType(), new ConstantBooleanType(false)); } public function getClass(): string { - return \SplFileObject::class; + return SplFileObject::class; } public function isMethodSupported(MethodReflection $methodReflection): bool @@ -70,7 +74,7 @@ private function getReturnType(): Type $builder->setOffsetValueType(new ConstantStringType($key), $valueType); } - return TypeCombinator::union($builder->getArray(), new ConstantBooleanType(false)); + return $builder->getArray(); } } diff --git a/src/Type/Php/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 new file mode 100644 index 0000000000..d556fff1be --- /dev/null +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -0,0 +1,73 @@ +getName() === 'str_pad'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return new StringType(); + } + + $inputType = $scope->getType($args[0]->value); + $lengthType = $scope->getType($args[1]->value); + + $accessoryTypes = []; + if ($inputType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($inputType->isNonEmptyString()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + + 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) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..98afacb7d7 --- /dev/null +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -0,0 +1,113 @@ +getName() === 'str_repeat'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return new StringType(); + } + + $multiplierType = $scope->getType($args[1]->value); + + if ((new ConstantIntegerType(0))->isSuperTypeOf($multiplierType)->yes()) { + return new ConstantStringType(''); + } + + if (IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($multiplierType)->yes()) { + return new NeverType(); + } + + $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()) { + 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 b2b1b42eb8..9c28bc3c69 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -4,8 +4,12 @@ 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; use PHPStan\Type\Constant\ConstantBooleanType; @@ -15,53 +19,99 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function array_is_list; +use function array_map; +use function array_unique; +use function count; +use function in_array; +use function mb_internal_encoding; +use function mb_str_split; +use function str_split; final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function isFunctionSupported(FunctionReflection $functionReflection): bool + use MbFunctionsReturnTypeExtensionTrait; + + public function __construct(private PhpVersion $phpVersion) { - return $functionReflection->getName() === 'str_split'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return in_array($functionReflection->getName(), ['str_split', 'mb_str_split'], true); + } - if (count($functionCall->args) < 1) { - return $defaultReturnType; + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; } - $splitLength = 1; - if (count($functionCall->args) >= 2) { - $splitLengthType = $scope->getType($functionCall->args[1]->value); - if (!$splitLengthType instanceof ConstantIntegerType) { - return $defaultReturnType; + if (count($functionCall->getArgs()) >= 2) { + $splitLengthType = $scope->getType($functionCall->getArgs()[1]->value); + if ($splitLengthType instanceof ConstantIntegerType) { + $splitLength = $splitLengthType->getValue(); + if ($splitLength < 1) { + return new ConstantBooleanType(false); + } } - $splitLength = $splitLengthType->getValue(); - if ($splitLength < 1) { - return new ConstantBooleanType(false); + } else { + $splitLength = 1; + } + + $encoding = null; + if ($functionReflection->getName() === 'mb_str_split') { + if (count($functionCall->getArgs()) >= 3) { + $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 null; + } + + $encoding = $values[0]; + if (!$this->isSupportedEncoding($encoding)) { + return new ConstantBooleanType(false); + } + } else { + $encoding = mb_internal_encoding(); } } - $stringType = $scope->getType($functionCall->args[0]->value); - if (!$stringType instanceof ConstantStringType) { - return new ArrayType(new IntegerType(), new StringType()); + if (!isset($splitLength)) { + return null; } - $stringValue = $stringType->getValue(); - $items = str_split($stringValue, $splitLength); - if (!is_array($items)) { - throw new \PHPStan\ShouldNotHappenException(); + $stringType = $scope->getType($functionCall->getArgs()[0]->value); + + $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()); + + return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray() + ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) + : $returnType; } /** * @param string[] $constantArray - * @param \PHPStan\Analyser\Scope $scope - * @return \PHPStan\Type\Constant\ConstantArrayType */ private static function createConstantArrayFrom(array $constantArray, Scope $scope): ConstantArrayType { @@ -73,7 +123,7 @@ private static function createConstantArrayFrom(array $constantArray, Scope $sco foreach ($constantArray as $key => $value) { $keyType = $scope->getTypeFromValue($key); if (!$keyType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $keyTypes[] = $keyType; @@ -83,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 new file mode 100644 index 0000000000..85efaa5ad2 --- /dev/null +++ b/src/Type/Php/StrTokFunctionReturnTypeExtension.php @@ -0,0 +1,45 @@ +getName() === 'strtok'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) !== 2) { + return null; + } + + $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); + $isEmptyString = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); + if ($isEmptyString->yes()) { + return new ConstantBooleanType(false); + } + + if ($isEmptyString->no()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return null; + } + +} diff --git a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php index 6e53059859..33ed245739 100644 --- a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php @@ -2,18 +2,21 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function count; -class StrWordCountFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,15 +26,15 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope + Node\Expr\FuncCall $functionCall, + Scope $scope, ): Type { - $argsCount = count($functionCall->args); + $argsCount = count($functionCall->getArgs()); if ($argsCount === 1) { return new IntegerType(); } elseif ($argsCount === 2 || $argsCount === 3) { - $formatType = $scope->getType($functionCall->args[1]->value); + $formatType = $scope->getType($functionCall->getArgs()[1]->value); if ($formatType instanceof ConstantIntegerType) { $val = $formatType->getValue(); if ($val === 0) { diff --git a/src/Type/Php/StrlenFunctionReturnTypeExtension.php b/src/Type/Php/StrlenFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..bc91f8595b --- /dev/null +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -0,0 +1,79 @@ +getName() === 'strlen'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return null; + } + + $argType = $scope->getType($args[0]->value); + $constantScalars = $argType->getConstantScalarValues(); + + $lengths = []; + foreach ($constantScalars as $constantScalar) { + $stringScalar = (string) $constantScalar; + $length = strlen($stringScalar); + $lengths[] = $length; + } + + $isNonEmpty = $argType->isNonEmptyString(); + $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 $range; + } + +} diff --git a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php index 767c141307..084ee527af 100644 --- a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php @@ -8,12 +8,20 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; +use function array_map; +use function array_unique; +use function count; +use function gettype; +use function min; +use function strtotime; -class StrtotimeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,23 +31,40 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->args) === 0) { + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + if (count($functionCall->getArgs()) === 0) { return $defaultReturnType; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $result = array_unique(array_map(static function (ConstantStringType $string): bool { - return is_int(strtotime($string->getValue())); - }, TypeUtils::getConstantStrings($argType))); + $results = array_unique(array_map(static fn (ConstantStringType $string): int|bool => strtotime($string->getValue()), $argType->getConstantStrings())); + $resultTypes = array_unique(array_map(static fn (int|bool $value): string => gettype($value), $results)); - if (count($result) !== 1) { + if (count($resultTypes) !== 1 || count($results) === 0) { return $defaultReturnType; } - return $result[0] ? new IntegerType() : new ConstantBooleanType(false); + if ($results[0] === false) { + return new ConstantBooleanType(false); + } + + // 2nd param $baseTimestamp is too non-deterministic so simply return int + if (count($functionCall->getArgs()) > 1) { + return new IntegerType(); + } + + // if it is positive we can narrow down to positive-int as long as time flows forward + if (min(array_map('intval', $results)) > 0) { + return IntegerRangeType::createAllGreaterThan(0); + } + + return new IntegerType(); } } diff --git a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..8ed6c637fc --- /dev/null +++ b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php @@ -0,0 +1,63 @@ +getName(), self::FUNCTIONS, true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + if (count($functionCall->getArgs()) === 0) { + return new NullType(); + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + + switch ($functionReflection->getName()) { + case 'strval': + return $argType->toString(); + case 'intval': + $type = $argType->toInteger(); + return $type instanceof ErrorType ? new IntegerType() : $type; + case 'boolval': + return $argType->toBoolean(); + case 'floatval': + case 'doubleval': + $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 new file mode 100644 index 0000000000..df1d0cf060 --- /dev/null +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -0,0 +1,130 @@ +getName(), ['substr', 'mb_substr'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + 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(); + } + + $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()); + } + } + + if (is_bool($substr)) { + $results[] = new ConstantBooleanType($substr); + } else { + $results[] = new ConstantStringType($substr); + } + } + + return TypeCombinator::union(...$results); + } + + $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 (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + return TypeCombinator::union( + new ConstantBooleanType(false), + new IntersectionType($accessoryTypes), + ); + } + + return new IntersectionType($accessoryTypes); + } + + return null; + } + +} diff --git a/src/Type/Php/ThrowableReturnTypeExtension.php b/src/Type/Php/ThrowableReturnTypeExtension.php new file mode 100644 index 0000000000..9437b82485 --- /dev/null +++ b/src/Type/Php/ThrowableReturnTypeExtension.php @@ -0,0 +1,77 @@ +getName() === 'getCode'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $type = $scope->getType($methodCall->var); + $types = []; + $pdoException = new ObjectType('PDOException'); + foreach ($type->getObjectClassNames() as $class) { + $classType = new ObjectType($class); + if ($classType->getClassReflection() !== null) { + $classReflection = $classType->getClassReflection(); + foreach ($classReflection->getMethodTags() as $methodName => $methodTag) { + if (strtolower($methodName) !== 'getcode') { + continue; + } + + $types[] = $methodTag->getReturnType(); + continue 2; + } + } + + if ($pdoException->isSuperTypeOf($classType)->yes()) { + $types[] = new BenevolentUnionType([new IntegerType(), new StringType()]); + continue; + } + + if (in_array(strtolower($class), [ + 'throwable', + 'exception', + 'runtimeexception', + ], true)) { + $types[] = new BenevolentUnionType([new IntegerType(), new StringType()]); + continue; + } + + $types[] = new IntegerType(); + } + + if (count($types) === 0) { + return new ErrorType(); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..c05c6f6cef --- /dev/null +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -0,0 +1,68 @@ +getName() === 'trigger_error'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + + if (count($args) === 0) { + return null; + } + + if (count($args) === 1) { + return new ConstantBooleanType(true); + } + + $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 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 9dc7d71a13..a6ab212328 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -6,34 +6,27 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; +use function count; +use function in_array; -class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension, BrokerAwareExtension +final class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension { - private bool $treatPhpDocTypesAsCertain; + private TypeSpecifier $typeSpecifier; - private \PHPStan\Broker\Broker $broker; + private ?ImpossibleCheckTypeHelper $helper = null; - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private ?\PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $helper = null; - - public function __construct(bool $treatPhpDocTypesAsCertain) - { - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - } - - public function setBroker(Broker $broker): void + /** + * @param string[] $universalObjectCratesClasses + */ + public function __construct(private ReflectionProvider $reflectionProvider, private bool $treatPhpDocTypesAsCertain, private array $universalObjectCratesClasses) { - $this->broker = $broker; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -45,42 +38,28 @@ 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); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) === 0) { + return null; } $isAlways = $this->getHelper()->findSpecifiedType( $scope, - $functionCall + $functionCall, ); if ($isAlways === null) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } return new ConstantBooleanType($isAlways); @@ -89,7 +68,7 @@ public function getTypeFromFunctionCall( private function getHelper(): ImpossibleCheckTypeHelper { if ($this->helper === null) { - $this->helper = new ImpossibleCheckTypeHelper($this->broker, $this->typeSpecifier, $this->broker->getUniversalObjectCratesClasses(), $this->treatPhpDocTypesAsCertain); + $this->helper = new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain); } return $this->helper; diff --git a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php deleted file mode 100644 index b90055cd8d..0000000000 --- a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php +++ /dev/null @@ -1,58 +0,0 @@ -getName(), - [ - 'var_export', - 'highlight_file', - 'highlight_string', - 'print_r', - ], - true - ); - } - - public function getTypeFromFunctionCall(\PHPStan\Reflection\FunctionReflection $functionReflection, \PhpParser\Node\Expr\FuncCall $functionCall, \PHPStan\Analyser\Scope $scope): \PHPStan\Type\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->args) < 1) { - return TypeCombinator::union( - new StringType(), - $fallbackReturnType - ); - } - - if (count($functionCall->args) < 2) { - return $fallbackReturnType; - } - - $returnArgumentType = $scope->getType($functionCall->args[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 d0a6c37763..7ca5d8bac9 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -2,20 +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 \PHPStan\Type\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'; @@ -24,41 +41,38 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope - ): Type + Scope $scope, + ): ?Type { - if (count($functionCall->args) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->args, $functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; } - $version1Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[0]->value)); - $version2Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[1]->value)); + $version1Strings = $this->getVersionStrings($args[0]->value, $scope); + $version2Strings = $this->getVersionStrings($args[1]->value, $scope); $counts = [ count($version1Strings), count($version2Strings), ]; - if (isset($functionCall->args[2])) { - $operatorStrings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[2]->value)); + if (isset($args[2])) { + $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); $returnType = new BooleanType(); } else { $returnType = TypeCombinator::union( new ConstantIntegerType(-1), new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } - if (count(array_filter($counts, static function (int $count): bool { - return $count === 0; - })) > 0) { + if (count(array_filter($counts, static fn (int $count): bool => $count === 0)) > 0) { return $returnType; // one of the arguments is not a constant string } - if (count(array_filter($counts, static function (int $count): bool { - return $count > 1; - })) > 1) { + if (count(array_filter($counts, static fn (int $count): bool => $count > 1)) > 1) { return $returnType; // more than one argument can have multiple possibilities, avoid combinatorial explosion } @@ -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 new file mode 100644 index 0000000000..26551ff830 --- /dev/null +++ b/src/Type/Php/XMLReaderOpenReturnTypeExtension.php @@ -0,0 +1,47 @@ +getName() === 'open'; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $this->isMethodSupported($methodReflection); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return new BooleanType(); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + { + return new UnionType([new ObjectType(self::XML_READER_CLASS), new ConstantBooleanType(false)]); + } + +} diff --git a/src/Type/RecursionGuard.php b/src/Type/RecursionGuard.php index 6e1cf7191a..aeecba0db7 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -2,19 +2,18 @@ namespace PHPStan\Type; -class RecursionGuard +final class RecursionGuard { /** @var true[] */ private static array $context = []; /** - * @param Type $type - * @param callable(): Type $callback - * - * @return Type + * @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 bee923df8f..4ed9c79c1c 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -2,35 +2,64 @@ 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; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +/** @api */ class ResourceType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + public function __construct() + { + } 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(); @@ -51,37 +80,50 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + [], + TrinaryLogic::createYes(), ); } - public function isOffsetAccessible(): TrinaryLogic + public function toArrayKey(): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + return $this; } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return new ErrorType(); + return new BooleanType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function exponentiate(Type $exponent): Type { return new ErrorType(); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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 = $baseClass; + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + + $this->subtractedType = $subtractedType; + $this->baseClass = $classReflection->getName(); } public function getClassName(): string @@ -31,65 +58,117 @@ public function getClassName(): string return $this->baseClass; } - public function getAncestorWithClassName(string $className): ?ObjectType + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getAncestorWithClassName(string $className): ?TypeWithClassName { - return $this->getStaticObjectType()->getAncestorWithClassName($className); + $ancestor = $this->getStaticObjectType()->getAncestorWithClassName($className); + if ($ancestor === null) { + return null; + } + + $classReflection = $ancestor->getClassReflection(); + if ($classReflection !== null) { + return $this->changeBaseClass($classReflection); + } + + return null; } public function getStaticObjectType(): ObjectType { if ($this->staticObjectType === null) { - $this->staticObjectType = new ObjectType($this->baseClass); + 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), + ); + } + + return $this->staticObjectType = new ObjectType($this->classReflection->getName(), $this->subtractedType, $this->classReflection); } return $this->staticObjectType; } - /** - * @return string[] - */ public function getReferencedClasses(): array { return $this->getStaticObjectType()->getReferencedClasses(); } - public function getBaseClass(): string + public function getObjectClassNames(): array { - return $this->baseClass; + return $this->getStaticObjectType()->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->getStaticObjectType()->getObjectClassReflections(); + } + + public function getArrays(): array + { + return $this->getStaticObjectType()->getArrays(); + } + + public function getConstantArrays(): array + { + return $this->getStaticObjectType()->getConstantArrays(); + } + + public function getConstantStrings(): array + { + return $this->getStaticObjectType()->getConstantStrings(); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + 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) { - return TrinaryLogic::createMaybe()->and($this->getStaticObjectType()->isSuperTypeOf($type)); + $result = $this->getStaticObjectType()->isSuperTypeOf($type); + if ($result->yes()) { + $classReflection = $type->getClassReflection(); + if ($classReflection !== null && $classReflection->isFinal()) { + return $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 @@ -98,14 +177,27 @@ public function equals(Type $type): bool return false; } - /** @var StaticType $type */ - $type = $type; return $this->getStaticObjectType()->equals($type->getStaticObjectType()); } public function describe(VerbosityLevel $level): string { - return sprintf('static(%s)', $this->getClassName()); + 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 @@ -118,9 +210,31 @@ 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(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - return $this->getStaticObjectType()->getProperty($propertyName, $scope); + $staticObject = $this->getStaticObjectType(); + $nakedProperty = $staticObject->getUnresolvedPropertyPrototype($propertyName, $scope)->getNakedProperty(); + + $ancestor = $this->getAncestorWithClassName($nakedProperty->getDeclaringClass()->getName()); + $classReflection = null; + if ($ancestor !== null) { + $classReflection = $ancestor->getClassReflection(); + } + if ($classReflection === null) { + $classReflection = $nakedProperty->getDeclaringClass(); + } + + return new CallbackUnresolvedPropertyPrototypeReflection( + $nakedProperty, + $classReflection, + false, + fn (Type $type): Type => $this->transformStaticType($type, $scope), + ); } public function canCallMethods(): TrinaryLogic @@ -133,9 +247,53 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - return $this->getStaticObjectType()->getMethod($methodName, $scope); + $staticObject = $this->getStaticObjectType(); + $nakedMethod = $staticObject->getUnresolvedMethodPrototype($methodName, $scope)->getNakedMethod(); + + $ancestor = $this->getAncestorWithClassName($nakedMethod->getDeclaringClass()->getName()); + $classReflection = null; + if ($ancestor !== null) { + $classReflection = $ancestor->getClassReflection(); + } + if ($classReflection === null) { + $classReflection = $nakedMethod->getDeclaringClass(); + } + + return new CallbackUnresolvedMethodPrototypeReflection( + $nakedMethod, + $classReflection, + false, + fn (Type $type): Type => $this->transformStaticType($type, $scope), + ); + } + + private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scope): Type + { + return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($scope): Type { + if ($type instanceof StaticType) { + $classReflection = $this->classReflection; + $isFinal = false; + if ($scope->isInClass()) { + $classReflection = $scope->getClassReflection(); + $isFinal = $classReflection->isFinal(); + } + $type = $type->changeBaseClass($classReflection); + if (!$isFinal || $type instanceof ThisType) { + return $traverse($type); + } + + return $traverse($type->getStaticObjectType()); + } + + return $traverse($type); + }); } public function canAccessConstants(): TrinaryLogic @@ -148,14 +306,14 @@ 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); } public function changeBaseClass(ClassReflection $classReflection): self { - return new self($classReflection->getName()); + return new self($classReflection, $this->subtractedType); } public function isIterable(): TrinaryLogic @@ -168,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); @@ -193,9 +381,79 @@ public function getOffsetValueType(Type $offsetType): Type return $this->getStaticObjectType()->getOffsetValueType($offsetType); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + 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()->setOffsetValueType($offsetType, $valueType); + 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 @@ -203,15 +461,146 @@ public function isCallable(): TrinaryLogic return $this->getStaticObjectType()->isCallable(); } + public function getEnumCases(): array + { + return $this->getStaticObjectType()->getEnumCases(); + } + public function isArray(): TrinaryLogic { return $this->getStaticObjectType()->isArray(); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ + 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(); + } + + public function isNumericString(): TrinaryLogic + { + return $this->getStaticObjectType()->isNumericString(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->getStaticObjectType()->isNonEmptyString(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return $this->getStaticObjectType()->isNonFalsyString(); + } + + public function isLiteralString(): TrinaryLogic + { + return $this->getStaticObjectType()->isLiteralString(); + } + + 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); @@ -227,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(); @@ -247,18 +641,106 @@ 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(); + } + 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; } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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) { + $type = TypeCombinator::union($this->subtractedType, $type); + } + + return $this->changeSubtractedType($type); + } + + public function getTypeWithoutSubtractedType(): Type + { + return $this->changeSubtractedType(null); + } + + 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, $objectType->getSubtractedType()); + } + + return TypeCombinator::intersect($this, $objectType); + } + } + + return new self($this->classReflection, $subtractedType); + } + + public function getSubtractedType(): ?Type + { + return $this->subtractedType; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->getStaticObjectType()->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + + public function exponentiate(Type $exponent): Type + { + return $this->getStaticObjectType()->exponentiate($exponent); + } + + public function getFiniteTypes(): array + { + return $this->getStaticObjectType()->getFiniteTypes(); + } + + public function toPhpDocNode(): TypeNode { - return new self($properties['baseClass']); + return new IdentifierTypeNode('static'); } } diff --git a/src/Type/StaticTypeFactory.php b/src/Type/StaticTypeFactory.php new file mode 100644 index 0000000000..382e4208ad --- /dev/null +++ b/src/Type/StaticTypeFactory.php @@ -0,0 +1,44 @@ +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 @@ -61,9 +125,14 @@ 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(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -76,9 +145,14 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -91,9 +165,9 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isIterable(): TrinaryLogic @@ -116,16 +190,131 @@ 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(); + } + + 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 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(); @@ -136,7 +325,17 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return new ErrorType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); } @@ -166,6 +365,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -186,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(); @@ -196,18 +410,34 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return []; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } - /** - * @param mixed[] $properties - * @return Type - */ - 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 1be5164838..feb18e97d6 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -2,27 +2,55 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; -use PHPStan\TrinaryLogic; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; class StringAlwaysAcceptingObjectWithToStringType extends StringType { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if ($type instanceof TypeWithClassName) { - $broker = Broker::getInstance(); - if (!$broker->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 = $broker->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 dc1544757e..0f1778aa21 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,39 +2,69 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +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 { use JustNullableTypeTrait; use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; + use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + public function __construct() + { + } 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 @@ -43,10 +73,13 @@ 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): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($offsetType === null) { return new ErrorType(); @@ -57,36 +90,54 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType): Type 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 accepts(Type $type, bool $strictTypes): TrinaryLogic + 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): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } - if ($type instanceof TypeWithClassName && !$strictTypes) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($type->getClassName())) { - return TrinaryLogic::createNo(); - } + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } - $typeClass = $broker->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 @@ -94,6 +145,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); @@ -114,17 +170,157 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + [1], + [], + TrinaryLogic::createYes(), ); } - /** - * @param mixed[] $properties - * @return Type - */ - public static function __set_state(array $properties): Type + 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(); + } + + 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 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(''); + } + + return null; + } + + 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 8654a7ddc8..39d4949ca8 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -2,57 +2,87 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; -use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\Generic\TemplateTypeHelper; +use function sprintf; +/** @api */ class ThisType extends StaticType { - private ClassReflection $classReflection; - - private ?\PHPStan\Type\ObjectType $staticObjectType = null; - /** - * @param string|ClassReflection $classReflection + * @api */ - public function __construct($classReflection) + public function __construct( + ClassReflection $classReflection, + ?Type $subtractedType = null, + ) + { + parent::__construct($classReflection, $subtractedType); + } + + public function changeBaseClass(ClassReflection $classReflection): StaticType + { + return new self($classReflection, $this->getSubtractedType()); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('$this(%s)', $this->getStaticObjectType()->describe($level)); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - if (is_string($classReflection)) { - $classReflection = Broker::getInstance()->getClass($classReflection); + if ($type instanceof self) { + return $this->getStaticObjectType()->isSuperTypeOf($type); } - parent::__construct($classReflection->getName()); - $this->classReflection = $classReflection; + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + $parent = new parent($this->getClassReflection(), $this->getSubtractedType()); + + return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe()); } - public function getStaticObjectType(): ObjectType + public function changeSubtractedType(?Type $subtractedType): Type { - if ($this->staticObjectType === null) { - if ($this->classReflection->isGeneric()) { - $typeMap = $this->classReflection->getTemplateTypeMap()->map(static function (string $name, Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }); - return $this->staticObjectType = new GenericObjectType( - $this->classReflection->getName(), - $this->classReflection->typeMapToList($typeMap) - ); - } + $type = parent::changeSubtractedType($subtractedType); + if ($type instanceof parent) { + return new self($type->getClassReflection(), $subtractedType); + } + + return $type; + } - return $this->staticObjectType = new ObjectType($this->classReflection->getName(), null, $this->classReflection); + 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->staticObjectType; + return $this; } - public function changeBaseClass(ClassReflection $classReflection): StaticType + public function traverseSimultaneously(Type $right, callable $cb): Type { - return new self($classReflection); + if ($this->getSubtractedType() === null) { + return $this; + } + + return new self($this->getClassReflection()); } - public function describe(VerbosityLevel $level): string + public function toPhpDocNode(): TypeNode { - return sprintf('$this(%s)', $this->getClassName()); + 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 new file mode 100644 index 0000000000..c6efe96950 --- /dev/null +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -0,0 +1,78 @@ +value), + ]; + + 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(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + IntegerRangeType::createAllGreaterThan($this->value), + // subtract range when we support float-ranges + ]; + + if (!(bool) $this->value) { + $subtractedTypes[] = new ConstantBooleanType(true); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + 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), + ]; + + if ((bool) $this->value) { + $subtractedTypes[] = new ConstantBooleanType(true); + } + + return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); + } + + public function getGreaterOrEqualType(PhpVersion $phpVersion): Type + { + $subtractedTypes = [ + IntegerRangeType::createAllSmallerThan($this->value), + ]; + + 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 7fd1f9af7f..f458512622 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -2,42 +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\CompoundTypeHelper; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\ConstantScalarType; +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 CompoundTypeHelper::accepts($type, $this, $strictTypes); + 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 @@ -45,9 +74,55 @@ public function equals(Type $type): bool return $type instanceof self && $this->value === $type->value; } - public function generalize(): Type + 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, $phpVersion); + } + + return TrinaryLogic::createMaybe(); + } + + 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, $phpVersion); + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function getFiniteTypes(): array { - return 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 c5ba1d1421..4625da358b 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -2,18 +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(); @@ -24,9 +45,20 @@ 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 { - return new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function canCallMethods(): TrinaryLogic @@ -39,9 +71,20 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - return new DummyMethodReflection($methodName); + $method = new DummyMethodReflection($methodName); + return new CallbackUnresolvedMethodPrototypeReflection( + $method, + $method->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function canAccessConstants(): TrinaryLogic @@ -54,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 6859652d88..6e83395e95 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -24,7 +24,17 @@ public function getOffsetValueType(Type $offsetType): Type return new MixedType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + 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 $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 @@ +traverse(static fn (Type $type) => $type->generalize($precision)); + } + +} diff --git a/src/Type/Traits/NonIterableTypeTrait.php b/src/Type/Traits/NonIterableTypeTrait.php index 3d77453cd2..2a13430cc9 100644 --- a/src/Type/Traits/NonIterableTypeTrait.php +++ b/src/Type/Traits/NonIterableTypeTrait.php @@ -19,14 +19,39 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getArraySize(): Type + { + return new ErrorType(); + } + public function getIterableKeyType(): Type { return new ErrorType(); } + public function getFirstIterableKeyType(): Type + { + return new ErrorType(); + } + + public function getLastIterableKeyType(): Type + { + return new ErrorType(); + } + public function getIterableValueType(): Type { return new ErrorType(); } + public function getFirstIterableValueType(): Type + { + return new ErrorType(); + } + + public function getLastIterableValueType(): Type + { + return new ErrorType(); + } + } diff --git a/src/Type/Traits/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index 7bbcdd7577..d16b86c9b1 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -2,15 +2,30 @@ namespace PHPStan\Type\Traits; +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; use PHPStan\TrinaryLogic; +use PHPStan\Type\ErrorType; +use PHPStan\Type\Type; trait NonObjectTypeTrait { + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -21,9 +36,14 @@ 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(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -36,9 +56,14 @@ 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 \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection + { + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -51,9 +76,14 @@ public function hasConstant(string $constantName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection + { + throw new ShouldNotHappenException(); + } + + public function getConstantStrings(): array { - throw new \PHPStan\ShouldNotHappenException(); + return []; } public function isCloneable(): TrinaryLogic @@ -61,4 +91,14 @@ public function isCloneable(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getEnumCases(): array + { + return []; + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new ErrorType(); + } + } diff --git a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php index b40b5b17cb..d74dc8d0d1 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -24,9 +24,19 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this; + return new ErrorType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); } } diff --git a/src/Type/Traits/NonRemoveableTypeTrait.php b/src/Type/Traits/NonRemoveableTypeTrait.php new file mode 100644 index 0000000000..1eb40ea378 --- /dev/null +++ b/src/Type/Traits/NonRemoveableTypeTrait.php @@ -0,0 +1,15 @@ +getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection($propertyName); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function canCallMethods(): TrinaryLogic @@ -49,9 +83,20 @@ 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(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - return new DummyMethodReflection($methodName); + $method = new DummyMethodReflection($methodName); + return new CallbackUnresolvedMethodPrototypeReflection( + $method, + $method->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); } public function canAccessConstants(): TrinaryLogic @@ -64,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 @@ -74,19 +124,134 @@ 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(); + } + + 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 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 @@ -104,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 new file mode 100644 index 0000000000..6adf571d54 --- /dev/null +++ b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php @@ -0,0 +1,24 @@ + */ 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; @@ -31,43 +76,115 @@ 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; 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; 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): 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; /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return CallableParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array; @@ -85,6 +202,103 @@ public function toString(): Type; public function toArray(): Type; + public function toArrayKey(): Type; + + /** + * 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; + + public function isNumericString(): TrinaryLogic; + + public function isNonEmptyString(): TrinaryLogic; + + public function isNonFalsyString(): TrinaryLogic; + + public function isLiteralString(): TrinaryLogic; + + 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 getSmallerType(PhpVersion $phpVersion): Type; + + public function getSmallerOrEqualType(PhpVersion $phpVersion): 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 * @@ -107,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 * @@ -122,9 +338,21 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc public function traverse(callable $cb): Type; /** - * @param mixed[] $properties - * @return self + * Traverses inner types while keeping the same context in another type. + * + * @param callable(Type $left, Type $right): Type $cb */ - public static function __set_state(array $properties): self; + 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. + * + * @see TypeCombinator::remove() + */ + public function tryRemove(Type $typeToRemove): ?Type; + + public function generalize(GeneralizePrecision $precision): Type; } diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php new file mode 100644 index 0000000000..7dc7803af0 --- /dev/null +++ b/src/Type/TypeAlias.php @@ -0,0 +1,41 @@ +resolvedType = new CircularTypeAliasErrorType(); + return $self; + } + + public function resolve(TypeNodeResolver $typeNodeResolver): Type + { + if ($this->resolvedType === null) { + $this->resolvedType = $typeNodeResolver->resolve( + $this->typeNode, + $this->nameScope, + ); + } + + return $this->resolvedType; + } + +} diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php new file mode 100644 index 0000000000..af6be470cc --- /dev/null +++ b/src/Type/TypeAliasResolver.php @@ -0,0 +1,14 @@ +isSuperTypeOf($type)->no()) { + return self::union($type, $nullType); + } + + return $type; } public static function remove(Type $fromType, Type $typeToRemove): Type @@ -32,98 +67,52 @@ public static function remove(Type $fromType, Type $typeToRemove): Type return $fromType; } - if ($fromType instanceof UnionType) { - $innerTypes = []; - foreach ($fromType->getTypes() as $innerType) { - $innerTypes[] = self::remove($innerType, $typeToRemove); - } - - return self::union(...$innerTypes); - } - $isSuperType = $typeToRemove->isSuperTypeOf($fromType); if ($isSuperType->yes()) { return new NeverType(); } + if ($isSuperType->no()) { + return $fromType; + } - if ($fromType instanceof BooleanType) { - if ($typeToRemove instanceof ConstantBooleanType) { - return new ConstantBooleanType(!$typeToRemove->getValue()); - } - } elseif ($fromType instanceof IterableType) { - $traversableType = new ObjectType(\Traversable::class); - $arrayType = new ArrayType(new MixedType(), new MixedType()); - if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return $traversableType; - } - if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return $arrayType; - } - } elseif ($fromType instanceof IntegerRangeType || $fromType instanceof IntegerType) { - if ($fromType instanceof ConstantIntegerType) { - $minA = $fromType->getValue(); - $maxA = $fromType->getValue(); - } else { - $minA = $fromType instanceof IntegerRangeType ? $fromType->getMin() : PHP_INT_MIN; - $maxA = $fromType instanceof IntegerRangeType ? $fromType->getMax() : PHP_INT_MAX; + if ($typeToRemove instanceof MixedType) { + $typeToRemoveSubtractedType = $typeToRemove->getSubtractedType(); + if ($typeToRemoveSubtractedType !== null) { + return self::intersect($fromType, $typeToRemoveSubtractedType); } + } - if ($typeToRemove instanceof IntegerRangeType) { - $removeValueMin = $typeToRemove->getMin(); - $removeValueMax = $typeToRemove->getMax(); - if ($minA < $removeValueMin && $removeValueMax < $maxA) { - return self::union( - IntegerRangeType::fromInterval($minA, $removeValueMin - 1), - IntegerRangeType::fromInterval($removeValueMax + 1, $maxA) - ); - } - if ($removeValueMin <= $minA && $minA <= $removeValueMax) { - return IntegerRangeType::fromInterval( - $removeValueMax === PHP_INT_MAX ? PHP_INT_MAX : $removeValueMax + 1, - $maxA - ); - } - if ($removeValueMin <= $maxA && $maxA <= $removeValueMax) { - return IntegerRangeType::fromInterval( - $minA, - $removeValueMin === PHP_INT_MIN ? PHP_INT_MIN : $removeValueMin - 1 - ); - } - } elseif ($typeToRemove instanceof ConstantIntegerType) { - $removeValue = $typeToRemove->getValue(); - if ($minA < $removeValue && $removeValue < $maxA) { - return self::union( - IntegerRangeType::fromInterval($minA, $removeValue - 1), - IntegerRangeType::fromInterval($removeValue + 1, $maxA) - ); - } - if ($removeValue === $minA) { - return IntegerRangeType::fromInterval($minA + 1, $maxA); + $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 ($removeValue === $maxA) { - return IntegerRangeType::fromInterval($minA, $maxA - 1); + + if (count($result) === count($fromFiniteTypes)) { + return $fromType; } - } - } elseif ($fromType->isArray()->yes()) { - if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { - return self::intersect($fromType, new NonEmptyArrayType()); - } - if ($typeToRemove instanceof NonEmptyArrayType) { - return new ConstantArrayType([], []); - } + if (count($result) === 0) { + return new NeverType(); + } - if ($fromType instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { - return $fromType->unsetOffset($typeToRemove->getOffsetType()); - } - } elseif ($fromType instanceof SubtractableType) { - $typeToSubtractFrom = $fromType; - if ($fromType instanceof TemplateType) { - $typeToSubtractFrom = $fromType->getBound(); - } + if (count($result) === 1) { + return $result[0]; + } - if ($typeToSubtractFrom->isSuperTypeOf($typeToRemove)->yes()) { - return $fromType->subtract($typeToRemove); + return new UnionType($result); } } @@ -156,33 +145,51 @@ 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; // transform A | (B | C) to A | B | C - for ($i = 0; $i < count($types); $i++) { + for ($i = 0; $i < $typesCount; $i++) { if ($types[$i] instanceof BenevolentUnionType) { - foreach ($types[$i]->getTypes() as $benevolentInnerType) { + if ($types[$i] instanceof TemplateBenevolentUnionType && $benevolentUnionObject === null) { + $benevolentUnionObject = $types[$i]; + } + $benevolentTypesCount = 0; + $typesInner = $types[$i]->getTypes(); + foreach ($typesInner as $benevolentInnerType) { + $benevolentTypesCount++; $benevolentTypes[$benevolentInnerType->describe(VerbosityLevel::value())] = $benevolentInnerType; } - array_splice($types, $i, 1, $types[$i]->getTypes()); + array_splice($types, $i, 1, $typesInner); + $typesCount += $benevolentTypesCount - 1; continue; } if (!($types[$i] instanceof UnionType)) { continue; } + if ($types[$i] instanceof TemplateType) { + continue; + } - array_splice($types, $i, 1, $types[$i]->getTypes()); + $typesInner = $types[$i]->getTypes(); + array_splice($types, $i, 1, $typesInner); + $typesCount += count($typesInner) - 1; + } + + if ($typesCount === 1) { + return $types[0]; } - $typesCount = count($types); $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; @@ -201,189 +208,165 @@ 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]); } - /** @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]); + foreach ($scalarTypes as $classType => $scalarTypeItems) { + $scalarTypes[$classType] = array_values($scalarTypeItems); } - $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) ); - - // simplify string[] | int[] to (string|int)[] - for ($i = 0; $i < count($types); $i++) { - for ($j = $i + 1; $j < count($types); $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); - continue 2; - } - } - } + $types = array_merge($types, $integerRangeTypes); + $types = array_values($types); + $typesCount = count($types); foreach ($scalarTypes as $classType => $scalarTypeItems) { if (isset($hasGenericScalarTypes[$classType])) { + unset($scalarTypes[$classType]); continue; } if ($classType === ConstantBooleanType::class && count($scalarTypeItems) === 2) { $types[] = new BooleanType(); + $typesCount++; + unset($scalarTypes[$classType]); continue; } - foreach ($scalarTypeItems as $type) { - if (count($scalarTypeItems) > self::CONSTANT_SCALAR_UNION_THRESHOLD) { - $types[] = $type->generalize(); - if ($type instanceof ConstantStringType) { + $scalarTypeItemsCount = count($scalarTypeItems); + for ($i = 0; $i < $typesCount; $i++) { + for ($j = 0; $j < $scalarTypeItemsCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $scalarTypeItems[$j]); + if ($compareResult === null) { continue; } - break; + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($scalarTypeItems, $j--, 1); + $scalarTypeItemsCount--; + continue 1; + } + if ($b !== null) { + $scalarTypeItems[$j] = $b; + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } } - $types[] = $type; } + + $scalarTypes[$classType] = $scalarTypeItems; } + if (count($types) > 16) { + $newTypes = []; + foreach ($types as $type) { + $newTypes[$type->describe(VerbosityLevel::cache())] = $type; + } + $types = array_values($newTypes); + } + + $types = array_merge( + $types, + self::processArrayTypes($arrayTypes), + ); + $typesCount = count($types); + // transform A | A to A // transform A | never to A - for ($i = 0; $i < count($types); $i++) { - for ($j = $i + 1; $j < count($types); $j++) { - if ($types[$i] instanceof IntegerRangeType) { - if ($types[$j] instanceof IntegerRangeType) { - if ( - $types[$i]->isSuperTypeOf($types[$j])->maybe() || - $types[$i]->getMax() + 1 === $types[$j]->getMin() || - $types[$j]->getMax() + 1 === $types[$i]->getMin() - ) { - $types[$i] = IntegerRangeType::fromInterval( - min($types[$i]->getMin(), $types[$j]->getMin()), - max($types[$i]->getMax(), $types[$j]->getMax()) - ); - $i--; - array_splice($types, $j, 1); - continue 2; - } - } - - if ($types[$j] instanceof ConstantIntegerType) { - $value = $types[$j]->getValue(); - if ($types[$i]->getMin() === $value + 1) { - $types[$i] = IntegerRangeType::fromInterval($value, $types[$i]->getMax()); - $i--; - array_splice($types, $j, 1); - continue 2; - } - if ($types[$i]->getMax() === $value - 1) { - $types[$i] = IntegerRangeType::fromInterval($types[$i]->getMin(), $value); - $i--; - array_splice($types, $j, 1); - continue 2; - } - } + for ($i = 0; $i < $typesCount; $i++) { + for ($j = $i + 1; $j < $typesCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $types[$j]); + if ($compareResult === null) { + continue; } - if ($types[$i] instanceof SubtractableType) { - $typeWithoutSubtractedTypeA = $types[$i]->getTypeWithoutSubtractedType(); - if ($typeWithoutSubtractedTypeA instanceof MixedType && $types[$j] instanceof MixedType) { - $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($types[$j]); - } else { - $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($types[$j]); - } - if ($isSuperType->yes()) { - $subtractedType = null; - if ($types[$j] instanceof SubtractableType) { - $subtractedType = $types[$j]->getSubtractedType(); - } - $types[$i] = self::intersectWithSubtractedType($types[$i], $subtractedType); - array_splice($types, $j--, 1); - continue 1; - } + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($types, $j--, 1); + $typesCount--; + continue 1; } + if ($b !== null) { + $types[$j] = $b; + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + } + } - if ($types[$j] instanceof SubtractableType) { - $typeWithoutSubtractedTypeB = $types[$j]->getTypeWithoutSubtractedType(); - if ($typeWithoutSubtractedTypeB instanceof MixedType && $types[$i] instanceof MixedType) { - $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($types[$i]); - } else { - $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($types[$i]); - } - if ($isSuperType->yes()) { - $subtractedType = null; - if ($types[$i] instanceof SubtractableType) { - $subtractedType = $types[$i]->getSubtractedType(); - } - $types[$j] = self::intersectWithSubtractedType($types[$j], $subtractedType); - array_splice($types, $i--, 1); - continue 2; - } + $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; } - if ( - !$types[$j] instanceof ConstantArrayType - && $types[$j]->isSuperTypeOf($types[$i])->yes() - ) { + [$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; } + } + } - if ( - !$types[$i] instanceof ConstantArrayType - && $types[$i]->isSuperTypeOf($types[$j])->yes() - ) { - array_splice($types, $j--, 1); - continue 1; - } + foreach ($enumCaseTypes as $enumCaseType) { + $types[] = $enumCaseType; + $typesCount++; + } + + foreach ($scalarTypes as $scalarTypeItems) { + foreach ($scalarTypeItems as $scalarType) { + $types[] = $scalarType; + $typesCount++; } } - if (count($types) === 0) { + if ($typesCount === 0) { return new NeverType(); - - } elseif (count($types) === 1) { + } + if ($typesCount === 1) { return $types[0]; } - if (count($benevolentTypes) > 0) { + if ($benevolentTypes !== []) { $tempTypes = $types; foreach ($tempTypes as $i => $type) { if (!isset($benevolentTypes[$type->describe(VerbosityLevel::value())])) { @@ -393,17 +376,164 @@ public static function union(Type ...$types): Type unset($tempTypes[$i]); } - if (count($tempTypes) === 0) { - return new BenevolentUnionType($types); + if ($tempTypes === []) { + if ($benevolentUnionObject instanceof TemplateBenevolentUnionType) { + return $benevolentUnionObject->withTypes($types); + } + + return new BenevolentUnionType($types, true); } } - return new UnionType($types); + return new UnionType($types, true); + } + + /** + * @return array{Type, null}|array{null, Type}|null + */ + private static function compareTypesInUnion(Type $a, Type $b): ?array + { + if ($a instanceof IntegerRangeType) { + $type = $a->tryUnion($b); + if ($type !== null) { + $a = $type; + return [$a, null]; + } + } + if ($b instanceof IntegerRangeType) { + $type = $b->tryUnion($a); + if ($type !== null) { + $b = $type; + return [null, $b]; + } + } + if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) { + return null; + } + if ($a instanceof 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(); + if ($typeWithoutSubtractedTypeA instanceof MixedType && $b instanceof MixedType) { + $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($b); + } else { + $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b); + } + if ($isSuperType->yes()) { + $a = self::intersectWithSubtractedType($a, $b); + return [$a, null]; + } + } + + if ($b instanceof SubtractableType) { + $typeWithoutSubtractedTypeB = $b->getTypeWithoutSubtractedType(); + if ($typeWithoutSubtractedTypeB instanceof MixedType && $a instanceof MixedType) { + $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($a); + } else { + $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a); + } + if ($isSuperType->yes()) { + $b = self::intersectWithSubtractedType($b, $a); + return [null, $b]; + } + } + + if ($b->isSuperTypeOf($a)->yes()) { + return [null, $b]; + } + + 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-falsy-string') + ) { + 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-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 [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 + ?Type $subtractedType, ): Type { if ($subtractedType === null) { @@ -411,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; } @@ -430,62 +555,180 @@ private static function unionWithSubtractedType( return new NeverType(); } - return $type; + return self::remove($type, $subtractedType); } private static function intersectWithSubtractedType( - SubtractableType $subtractableType, - ?Type $subtractedType + SubtractableType $a, + Type $b, ): Type { - if ($subtractableType->getSubtractedType() === null) { - return $subtractableType; + if ($a->getSubtractedType() === null) { + return $a; } - if ($subtractedType === null) { - return $subtractableType->getTypeWithoutSubtractedType(); + if ($b instanceof IntersectionType) { + $subtractableTypes = []; + foreach ($b->getTypes() as $innerType) { + if (!$innerType instanceof SubtractableType) { + continue; + } + + $subtractableTypes[] = $innerType; + } + + if (count($subtractableTypes) === 0) { + return $a->getTypeWithoutSubtractedType(); + } + + $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 = self::intersect( - $subtractableType->getSubtractedType(), - $subtractedType + $a->getSubtractedType(), + $subtractedType, ); if ($subtractedType instanceof NeverType) { $subtractedType = null; } - return $subtractableType->changeSubtractedType($subtractedType); + return $a->changeSubtractedType($subtractedType); } /** - * @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 (count($arrayTypes) === 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 []; - } elseif (count($arrayTypes) === 1) { + } + + $accessoryTypes = self::processArrayAccessoryTypes($arrayTypes); + + if (count($arrayTypes) === 1) { return [ - self::intersect($arrayTypes[0], ...$accessoryTypes), + self::intersect(...$arrayTypes, ...$accessoryTypes), ]; } @@ -493,125 +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 $arrayIdx => $arrayType) { + $constantArrays = $constantArraysMap[$arrayIdx]; + $isConstantArray = $constantArrays !== []; + if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) { + $filledArrays++; + } - foreach ($arrayTypes as $arrayType) { - if (!$arrayType instanceof ConstantArrayType || $generalArrayOccurred) { - $keyTypesForGeneralArray[] = $arrayType->getKeyType(); - $valueTypesForGeneralArray[] = $arrayType->getItemType(); - $generalArrayOccurred = true; + 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; + } + + $arraysToProcessPerKey = []; + foreach ($arraysToProcess as $i => $arrayToProcess) { + foreach ($arrayToProcess->getKeyTypes() as $keyType) { + $arraysToProcessPerKey[$keyType->getValue()][] = $i; + } } - for ($i = 0; $i < count($arraysToProcess); $i++) { - for ($j = $i + 1; $j < count($arraysToProcess); $j++) { - if ($arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$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); + 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); + 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; + } } } @@ -620,31 +1054,129 @@ private static function reduceArrays(array $constantArrays): array 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; + } + + if ($a instanceof TemplateType) { + return -1; + } + if ($b instanceof TemplateType) { + return 1; + } + + if ($a instanceof BenevolentUnionType) { + return -1; + } + if ($b instanceof BenevolentUnionType) { + return 1; + } + + return 0; + }; + usort($types, $sortTypes); // transform A & (B | C) to (A & B) | (A & C) foreach ($types as $i => $type) { - if ($type instanceof UnionType) { - $topLevelUnionSubTypes = []; - foreach ($type->getTypes() as $innerUnionSubType) { - $topLevelUnionSubTypes[] = self::intersect( - $innerUnionSubType, - ...array_slice($types, 0, $i), - ...array_slice($types, $i + 1) - ); - } + if (!$type instanceof UnionType) { + continue; + } + + $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, + ...$slice1, + ...$slice2, + ); + } + + $union = self::union(...$topLevelUnionSubTypes); + if ($union instanceof NeverType) { + return $union; + } + + if ($type instanceof BenevolentUnionType) { + $union = TypeUtils::toBenevolentUnion($union); + } - return self::union(...$topLevelUnionSubTypes); + if ($type instanceof TemplateUnionType || $type instanceof TemplateBenevolentUnionType) { + $union = TemplateTypeFactory::create( + $type->getScope(), + $type->getName(), + $union, + $type->getVariance(), + $type->getStrategy(), + $type->getDefault(), + ); } + + return $union; } + $typesCount = count($types); // transform A & (B & C) to A & B & C - foreach ($types as $i => &$type) { + for ($i = 0; $i < $typesCount; $i++) { + $type = $types[$i]; + if (!($type instanceof IntersectionType)) { continue; } - array_splice($types, $i, 1, $type->getTypes()); + array_splice($types, $i--, 1, $type->getTypes()); + $typesCount = count($types); + } + + $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; + } + if ($b instanceof SubtractableType && $b->getSubtractedType() !== null) { + return 1; + } + + if ($a instanceof ConstantArrayType && !$b instanceof ConstantArrayType) { + return -1; + } + if ($b instanceof ConstantArrayType && !$a instanceof ConstantArrayType) { + return 1; + } + + return 0; + }); + // transform IntegerType & ConstantIntegerType to ConstantIntegerType // transform Child & Parent to Child // transform Object & ~null to Object @@ -653,8 +1185,8 @@ public static function intersect(Type ...$types): Type // transform callable & int to never // transform A & ~A to never // transform int & string to never - for ($i = 0; $i < count($types); $i++) { - for ($j = $i + 1; $j < count($types); $j++) { + for ($i = 0; $i < $typesCount; $i++) { + for ($j = $i + 1; $j < $typesCount; $j++) { if ($types[$j] instanceof SubtractableType) { $typeWithoutSubtractedTypeA = $types[$j]->getTypeWithoutSubtractedType(); @@ -666,6 +1198,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeSubtractableA->yes()) { $types[$i] = self::unionWithSubtractedType($types[$i], $types[$j]->getSubtractedType()); array_splice($types, $j--, 1); + $typesCount--; continue 1; } } @@ -681,9 +1214,21 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeSubtractableB->yes()) { $types[$j] = self::unionWithSubtractedType($types[$j], $types[$i]->getSubtractedType()); array_splice($types, $i--, 1); + $typesCount--; continue 2; } } + + if ($types[$i] instanceof IntegerRangeType) { + $intersectionType = $types[$i]->tryIntersect($types[$j]); + if ($intersectionType !== null) { + $types[$j] = $intersectionType; + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + } + if ($types[$j] instanceof IterableType) { $isSuperTypeA = $types[$j]->isSuperTypeOfMixed($types[$i]); } else { @@ -692,6 +1237,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeA->yes()) { array_splice($types, $j--, 1); + $typesCount--; continue; } @@ -702,21 +1248,165 @@ public static function intersect(Type ...$types): Type } if ($isSuperTypeB->maybe()) { - if ($types[$i] instanceof IntegerRangeType && $types[$j] instanceof IntegerRangeType) { - $min = max($types[$i]->getMin(), $types[$j]->getMin()); - $max = min($types[$i]->getMax(), $types[$j]->getMax()); - $types[$j] = IntegerRangeType::fromInterval($min, $max); - array_splice($types, $i--, 1); - continue 2; - } if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetType) { $types[$i] = $types[$i]->makeOffsetRequired($types[$j]->getOffsetType()); array_splice($types, $j--, 1); + $typesCount--; + continue; } if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetType) { $types[$j] = $types[$j]->makeOffsetRequired($types[$i]->getOffsetType()); array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ( + $types[$i] instanceof ConstantArrayType + && count($types[$i]->getKeyTypes()) === 1 + && $types[$i]->isOptionalKey(0) + && $types[$j] instanceof NonEmptyArrayType + ) { + $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); + } else { + $types[$j] = new ArrayType($keyType, $itemType); + } + array_splice($types, $i--, 1); + $typesCount--; + 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; @@ -724,6 +1414,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeB->yes()) { array_splice($types, $i--, 1); + $typesCount--; continue 2; } @@ -733,12 +1424,21 @@ public static function intersect(Type ...$types): Type } } - if (count($types) === 1) { + if ($typesCount === 1) { return $types[0]; - } 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 f1c6855fc4..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 */ @@ -30,6 +30,7 @@ class TypeTraverser * return new MixedType(); * }); * + * @api * @param callable(Type $type, callable(Type): Type $traverse): Type $cb */ public static function map(Type $type, callable $cb): Type diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index f9055a4e89..7213a8140f 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -5,189 +5,42 @@ use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantStringType; - -class TypeUtils +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 + */ +final class TypeUtils { /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ArrayType[] - */ - public static function getArrays(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 []; - } - - /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Constant\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 []; - } - - /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Constant\ConstantStringType[] - */ - public static function getConstantStrings(Type $type): array - { - return self::map(ConstantStringType::class, $type, false); - } - - /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ConstantType[] - */ - public static function getConstantTypes(Type $type): array - { - return self::map(ConstantType::class, $type, false); - } - - /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ConstantType[] - */ - public static function getAnyConstantTypes(Type $type): array - { - return self::map(ConstantType::class, $type, false, false); - } - - /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ArrayType[] - */ - public static function getAnyArrays(Type $type): array - { - return self::map(ArrayType::class, $type, true, false); - } - - public static function generalizeType(Type $type): Type - { - if ($type instanceof ConstantType) { - return $type->generalize(); - } elseif ($type instanceof UnionType) { - return TypeCombinator::union(...array_map(static function (Type $innerType): Type { - return self::generalizeType($innerType); - }, $type->getTypes())); - } - - return $type; - } - - /** - * @param Type $type - * @return string[] + * @return list */ - public static function getDirectClassNames(Type $type): array + public static function getConstantIntegers(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 self::map(ConstantIntegerType::class, $type, false); } /** - * @param Type $type - * @return \PHPStan\Type\ConstantScalarType[] + * @return list */ - public static function getConstantScalars(Type $type): array + public static function getIntegerRanges(Type $type): array { - return self::map(ConstantScalarType::class, $type, false); + return self::map(IntegerRangeType::class, $type, false); } /** - * @internal - * @param Type $type - * @return ConstantArrayType[] - */ - public static function getOldConstantArrays(Type $type): array - { - return self::map(ConstantArrayType::class, $type, false); - } - - /** - * @param string $typeClass - * @param Type $type - * @param bool $inspectIntersections - * @param bool $stopOnUnmatched - * @return mixed[] + * @return list */ private static function map( string $typeClass, Type $type, bool $inspectIntersections, - bool $stopOnUnmatched = true + bool $stopOnUnmatched = true, ): array { if ($type instanceof $typeClass) { @@ -197,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 []; } @@ -205,7 +60,9 @@ private static function map( continue; } - $matchingTypes[] = $innerType; + foreach ($matchingInner as $innerMapped) { + $matchingTypes[] = $innerMapped; + } } return $matchingTypes; @@ -245,7 +102,29 @@ public static function toBenevolentUnion(Type $type): Type } /** - * @param Type $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[] */ public static function flattenTypes(Type $type): array @@ -292,7 +171,6 @@ public static function findThisType(Type $type): ?ThisType } /** - * @param Type $type * @return HasPropertyType[] */ public static function getHasPropertyTypes(Type $type): array @@ -314,29 +192,47 @@ public static function getHasPropertyTypes(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Accessory\AccessoryType[] + * @return AccessoryType[] */ public static function getAccessoryTypes(Type $type): array { 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/TypeWithClassName.php b/src/Type/TypeWithClassName.php index d85966bb48..c2688af355 100644 --- a/src/Type/TypeWithClassName.php +++ b/src/Type/TypeWithClassName.php @@ -2,11 +2,16 @@ namespace PHPStan\Type; +use PHPStan\Reflection\ClassReflection; + +/** @api */ interface TypeWithClassName extends Type { public function getClassName(): string; - public function getAncestorWithClassName(string $className): ?ObjectType; + public function getAncestorWithClassName(string $className): ?self; + + public function getClassReflection(): ?ClassReflection; } diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index a5516a34a1..68ab00e39c 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -2,83 +2,72 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +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\ConstantArrayType; use PHPStan\Type\Generic\TemplateTypeHelper; -use ReflectionNamedType; +use ReflectionType; +use function array_map; +use function count; +use function get_class; +use function sprintf; -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 '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': - $broker = Broker::getInstance(); - if ($selfClass !== null && $broker->hasClass($selfClass)) { - $classReflection = $broker->getClass($selfClass); - if ($classReflection->getParentClass() !== false) { - return new ObjectType($classReflection->getParentClass()->getName()); - } - } - return new NonexistentParentClassType(); - default: - return new ObjectType($typeString); - } - } - + /** @api */ public static function decideTypeFromReflection( - ?\ReflectionType $reflectionType, + ?ReflectionType $reflectionType, ?Type $phpDocType = null, - ?string $selfClass = null, - bool $isVariadic = false + 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(); } - if (!$reflectionType instanceof ReflectionNamedType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); + if ($reflectionType instanceof ReflectionUnionType) { + $type = TypeCombinator::union(...array_map(static fn (ReflectionType $type): Type => self::decideTypeFromReflection($type, null, $selfClass, false), $reflectionType->getTypes())); + + return self::decideType($type, $phpDocType); } - $reflectionTypeString = $reflectionType->getName(); - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\object')) { - $reflectionTypeString = 'object'; + if ($reflectionType instanceof ReflectionIntersectionType) { + $types = []; + foreach ($reflectionType->getTypes() as $innerReflectionType) { + $innerType = self::decideTypeFromReflection($innerReflectionType, null, $selfClass, false); + if (!$innerType->isObject()->yes()) { + return new NeverType(); + } + + $types[] = $innerType; + } + + return self::decideType(TypeCombinator::intersect(...$types), $phpDocType); } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\mixed')) { - $reflectionTypeString = 'mixed'; + + if (!$reflectionType instanceof ReflectionNamedType) { + throw new ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); } - $type = self::getTypeObjectFromTypehint($reflectionTypeString, $selfClass); + if ($reflectionType->isIdentifier()) { + $typeNode = new Identifier($reflectionType->getName()); + } else { + $typeNode = new FullyQualified($reflectionType->getName()); + } + + $type = ParserNodeTypeToPHPStanType::resolve($typeNode, $selfClass); if ($reflectionType->allowsNull()) { $type = TypeCombinator::addNull($type); - } elseif ($phpDocType !== null) { - $phpDocType = TypeCombinator::removeNull($phpDocType); } return self::decideType($type, $phpDocType); @@ -86,39 +75,78 @@ 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 ($type instanceof VoidType || $phpDocType instanceof VoidType) { - return new VoidType(); + if ($phpDocType instanceof NeverType && $phpDocType->isExplicit()) { + return $phpDocType; + } + if ( + $type instanceof MixedType + && !$type->isExplicitMixed() + && $phpDocType->isVoid()->yes() + ) { + return $phpDocType; } if (TypeCombinator::removeNull($type) instanceof IterableType) { 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->getItemType() + $innerType->getIterableKeyType(), + $innerType->getItemType(), ); } else { $innerTypes[] = $innerType; } } $phpDocType = new UnionType($innerTypes); - } elseif ($phpDocType instanceof ArrayType) { + } elseif ($phpDocType instanceof ArrayType || $phpDocType instanceof ConstantArrayType) { $phpDocType = new IterableType( $phpDocType->getKeyType(), - $phpDocType->getItemType() + $phpDocType->getItemType(), ); } } - $resultType = $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() ? $phpDocType : $type; + if ( + ($type->isCallable()->yes() && $phpDocType->isCallable()->yes()) + || ( + (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) + && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ) + ) { + $resultType = $phpDocType; + } else { + $resultType = $type; + } + + if ($type instanceof UnionType) { + $addToUnionTypes = []; + foreach ($type->getTypes() as $innerType) { + if (!$innerType->isSuperTypeOf($resultType)->no()) { + continue; + } - if (TypeCombinator::containsNull($type)) { + $addToUnionTypes[] = $innerType; + } + + if (count($addToUnionTypes) > 0) { + $type = TypeCombinator::union($resultType, ...$addToUnionTypes); + } else { + $type = $resultType; + } + } elseif (TypeCombinator::containsNull($type)) { $type = TypeCombinator::addNull($resultType); } else { $type = $resultType; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index b66e64aa1e..08d678152a 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -2,34 +2,73 @@ namespace PHPStan\Type; +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\PropertyReflection; -use PHPStan\Reflection\Type\UnionTypeMethodReflection; +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 str_contains; + +/** @api */ class UnionType implements CompoundType { - /** @var \PHPStan\Type\Type[] */ - private array $types; + use NonGeneralizableTypeTrait; + + 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 \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Cannot create %s with: %s', self::class, - implode(', ', array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::value()); - }, $types)) + implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)), )); }; if (count($types) < 2) { @@ -39,14 +78,16 @@ public function __construct(array $types) if (!($type instanceof UnionType)) { continue; } + if ($type instanceof TemplateType) { + continue; + } $throwException(); } - $this->types = UnionTypeHelper::sortTypes($types); } /** - * @return \PHPStan\Type\Type[] + * @return Type[] */ public function getTypes(): array { @@ -54,59 +95,180 @@ public function getTypes(): array } /** - * @return string[] + * @param callable(Type $type): bool $filterCb */ - public function getReferencedClasses(): array + 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 UnionTypeHelper::getReferencedClasses($this->getTypes()); + return $this->normalized; } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + /** + * @return Type[] + */ + protected function getSortedTypes(): array { - if ($type instanceof CompoundType && !$type instanceof CallableType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + if ($this->sortedTypes) { + return $this->types; } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->accepts($type, $strictTypes); + $this->types = UnionTypeHelper::sortTypes($this->types); + $this->sortedTypes = true; + + return $this->types; + } + + public function getReferencedClasses(): array + { + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } } - return TrinaryLogic::createNo()->or(...$results); + return $classes; } - public function isSuperTypeOf(Type $otherType): TrinaryLogic + public function getObjectClassNames(): array { - if ($otherType instanceof self || $otherType instanceof IterableType) { - return $otherType->isSubTypeOf($this); + 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; } - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $innerType->isSuperTypeOf($otherType); + $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; + } + + if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + if ($type instanceof TemplateUnionType) { + 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 isSubTypeOf(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[] = $otherType->isSuperTypeOf($innerType); + foreach ($this->types as $innerType) { + $result = $innerType->isSuperTypeOf($otherType); + if ($result->yes()) { + return $result; + } + $results[] = $result; } + $result = IsSuperTypeOfResult::createNo()->or(...$results); - return TrinaryLogic::extremeIdentity(...$results); + if ($otherType instanceof TemplateUnionType) { + return $result->or($otherType->isSubTypeOf($this)); + } + + return $result; } - public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { - $results = []; - foreach ($this->getTypes() as $innerType) { - $results[] = $acceptingType->accepts($innerType, $strictTypes); - } + return IsSuperTypeOfResult::extremeIdentity(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types)); + } - return TrinaryLogic::extremeIdentity(...$results); + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types)); } public function equals(Type $type): bool @@ -119,91 +281,140 @@ 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) { - if ($type instanceof IntersectionType || $type instanceof ClosureType || $type instanceof CallableType) { + 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 (str_contains($intersectionDescription, '&')) { + $typeNames[] = sprintf('(%s)', $type->describe($level)); + } else { + $typeNames[] = $intersectionDescription; + } } else { $typeNames[] = $type->describe($level); } } + 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(); + 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]); }, - function () use ($joinTypes): string { - return $joinTypes($this->types); - } + fn (): string => $joinTypes($this->getSortedTypes()), ); } /** * @param callable(Type $type): TrinaryLogic $canCallback * @param callable(Type $type): TrinaryLogic $hasCallback - * @return TrinaryLogic */ private function hasInternal( callable $canCallback, - callable $hasCallback + callable $hasCallback, ): TrinaryLogic { - $results = []; - 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 - * @return object + * @param callable(Type $type): TObject $getCallback + * @return TObject */ private function getInternal( callable $hasCallback, - callable $getCallback - ) + callable $getCallback, + ): object { /** @var TrinaryLogic|null $result */ $result = null; - /** @var object|null $object */ + /** @var TObject|null $object */ $object = null; foreach ($this->types as $type) { $has = $hasCallback($type); @@ -220,153 +431,269 @@ private function getInternal( } if ($object === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } 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 function (Type $type): TrinaryLogic { - return $type->canAccessProperties(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); } public function hasProperty(string $propertyName): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($propertyName): TrinaryLogic { - return $type->hasProperty($propertyName); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - return $this->getInternal( - static function (Type $type) use ($propertyName): TrinaryLogic { - return $type->hasProperty($propertyName); - }, - static function (Type $type) use ($propertyName, $scope): PropertyReflection { - return $type->getProperty($propertyName, $scope); + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasProperty($propertyName)->yes()) { + continue; } - ); + + $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new ShouldNotHappenException(); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); } public function canCallMethods(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canCallMethods(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); } public function hasMethod(string $methodName): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($methodName): TrinaryLogic { - return $type->hasMethod($methodName); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { - $methods = []; + return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); + } + + public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection + { + $methodPrototypes = []; foreach ($this->types as $type) { if (!$type->hasMethod($methodName)->yes()) { continue; } - $methods[] = $type->getMethod($methodName, $scope); + $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this); } - $methodsCount = count($methods); + $methodsCount = count($methodPrototypes); if ($methodsCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($methodsCount === 1) { - return $methods[0]; + return $methodPrototypes[0]; } - return new UnionTypeMethodReflection($methodName, $methods); + return new UnionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes); } public function canAccessConstants(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); } public function hasConstant(string $constantName): TrinaryLogic { return $this->hasInternal( - static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }, - static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - } + static fn (Type $type): TrinaryLogic => $type->canAccessConstants(), + static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), ); } - public function getConstant(string $constantName): ConstantReflection + public function getConstant(string $constantName): ClassConstantReflection { return $this->getInternal( - static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - }, - static function (Type $type) use ($constantName): ConstantReflection { - return $type->getConstant($constantName); - } + static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), + static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName), ); } public function isIterable(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isIterable(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable()); } public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isIterableAtLeastOnce(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); + } + + public function getArraySize(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize()); } public function getIterableKeyType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getIterableKeyType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType()); + } + + public function 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 function (Type $type): Type { - return $type->getIterableValueType(); - }); + 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 function (Type $type): TrinaryLogic { - return $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->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString()); + } + + public function isNumericString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); + } + + public function isNonEmptyString(): TrinaryLogic + { + 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->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 { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isOffsetAccessible(); - }); + 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 function (Type $type) use ($offsetType): TrinaryLogic { - return $type->hasOffsetValueType($offsetType); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } public function getOffsetValueType(Type $offsetType): Type @@ -388,104 +715,304 @@ public function getOffsetValueType(Type $offsetType): Type return TypeCombinator::union(...$types); } - public function setOffsetValueType(?Type $offsetType, Type $valueType): Type + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this->unionTypes(static function (Type $type) use ($offsetType, $valueType): Type { - return $type->setOffsetValueType($offsetType, $valueType); - }); + 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 function (Type $type): TrinaryLogic { - return $type->isCallable(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] - */ 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 \PHPStan\ShouldNotHappenException(); + if (count($acceptors) === 0) { + throw new ShouldNotHappenException(); + } + + return $acceptors; } public function isCloneable(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isCloneable(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); + } + + public function isSmallerThan(Type $otherType, 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->isNull()); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); + } + + public function getConstantScalarTypes(): array + { + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes()); + } + + public function getConstantScalarValues(): array + { + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues()); + } + + public function isTrue(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue()); + } + + public function isFalse(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse()); + } + + public function isBoolean(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean()); + } + + public function isFloat(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat()); + } + + public function isInteger(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); + } + + public function getSmallerType(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 { /** @var BooleanType $type */ - $type = $this->unionTypes(static function (Type $type): BooleanType { - return $type->toBoolean(); - }); + $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean()); return $type; } public function toNumber(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toNumber(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber()); + + return $type; + } + + public function toAbsoluteNumber(): Type + { + $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); return $type; } public function toString(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toString(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toString()); return $type; } public function toInteger(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toInteger(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger()); return $type; } public function toFloat(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toFloat(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat()); return $type; } public function toArray(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toArray(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray()); return $type; } + 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(); + if ($receivedType instanceof UnionType) { + $myTypes = []; + $remainingReceivedTypes = []; + foreach ($receivedType->getTypes() as $receivedInnerType) { + foreach ($this->types as $type) { + if ($type->isSuperTypeOf($receivedInnerType)->yes()) { + $types = $types->union($type->inferTemplateTypes($receivedInnerType)); + continue 2; + } + $myTypes[] = $type; + } + $remainingReceivedTypes[] = $receivedInnerType; + } + if (count($remainingReceivedTypes) === 0) { + return $types; + } + $receivedType = TypeCombinator::union(...$remainingReceivedTypes); + } else { + $myTypes = $this->types; + } - foreach ($this->types as $type) { + foreach ($myTypes as $type) { + if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) { + continue; + } + $types = $types->union($type->inferTemplateTypes($receivedType)); + } + + if (!$types->isEmpty()) { + return $types; + } + + foreach ($myTypes as $type) { $types = $types->union($type->inferTemplateTypes($receivedType)); } @@ -536,31 +1063,135 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof self) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeCombinator::union(...$types); + } + + return $this; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); + } + + public function exponentiate(Type $exponent): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent)); + } + + public function getFiniteTypes(): array + { + $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes()); + $uniquedTypes = []; + foreach ($types as $type) { + $uniquedTypes[md5($type->describe(VerbosityLevel::cache()))] = $type; + } + + if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + return array_values($uniquedTypes); + } + /** - * @param mixed[] $properties - * @return Type + * @param callable(Type $type): TrinaryLogic $getResult */ - public static function __set_state(array $properties): Type + protected function unionResults(callable $getResult): TrinaryLogic { - return new self($properties['types']); + return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult); } /** * @param callable(Type $type): TrinaryLogic $getResult - * @return TrinaryLogic */ - private 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); } /** * @param callable(Type $type): Type $getType - * @return Type */ 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 4b15316457..f91ea5cb45 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -3,35 +3,28 @@ 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 count; +use function strcasecmp; +use function usort; +use const PHP_INT_MIN; -class UnionTypeHelper +final class UnionTypeHelper { /** - * @param \PHPStan\Type\Type[] $types - * @return string[] + * @param Type[] $types + * @return Type[] */ - public static function getReferencedClasses(array $types): array + public static function sortTypes(array $types): array { - $subTypeClasses = []; - foreach ($types as $type) { - $subTypeClasses[] = $type->getReferencedClasses(); + if (count($types) > 1024) { + return $types; } - return array_merge(...$subTypeClasses); - } - - /** - * @param \PHPStan\Type\Type[] $types - * @return \PHPStan\Type\Type[] - */ - public static function sortTypes(array $types): array - { usort($types, static function (Type $a, Type $b): int { if ($a instanceof NullType) { return 1; @@ -41,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; @@ -73,30 +66,73 @@ public static function sortTypes(array $types): array || $b instanceof ConstantFloatType ) ) { - return (int) ($a->getValue() - $b->getValue()); + $cmp = $a->getValue() <=> $b->getValue(); + if ($cmp !== 0) { + return $cmp; + } + if ($a instanceof ConstantIntegerType && $b instanceof ConstantFloatType) { + return -1; + } + if ($b instanceof ConstantIntegerType && $a instanceof ConstantFloatType) { + return 1; + } + return 0; + } + + if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) { + return ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN); + } + + if ($a instanceof IntegerRangeType && $b instanceof IntegerType) { + return 1; + } + + if ($b instanceof IntegerRangeType && $a instanceof IntegerType) { + return -1; } if ($a instanceof ConstantStringType && $b instanceof ConstantStringType) { - return strcasecmp($a->getValue(), $b->getValue()); + 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())); + } + + if ( + ($a instanceof CallableType || $a instanceof ClosureType) + && ($b instanceof CallableType || $b instanceof ClosureType) + ) { + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } - return strcasecmp($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); + 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 new file mode 100644 index 0000000000..f4ad8abbb8 --- /dev/null +++ b/src/Type/UsefulTypeAliasResolver.php @@ -0,0 +1,153 @@ + */ + private array $resolvedGlobalTypeAliases = []; + + /** @var array */ + private array $resolvedLocalTypeAliases = []; + + /** @var array */ + private array $resolvingClassTypeAliases = []; + + /** @var array */ + private array $inProcess = []; + + /** + * @param array $globalTypeAliases + */ + public function __construct( + private array $globalTypeAliases, + private TypeStringResolver $typeStringResolver, + private TypeNodeResolver $typeNodeResolver, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool + { + $hasGlobalTypeAlias = array_key_exists($aliasName, $this->globalTypeAliases); + if ($hasGlobalTypeAlias) { + return true; + } + + if ($classNameScope === null || !$this->reflectionProvider->hasClass($classNameScope)) { + return false; + } + + $classReflection = $this->reflectionProvider->getClass($classNameScope); + $localTypeAliases = $classReflection->getTypeAliases(); + return array_key_exists($aliasName, $localTypeAliases); + } + + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + return $this->resolveLocalTypeAlias($aliasName, $nameScope) + ?? $this->resolveGlobalTypeAlias($aliasName, $nameScope); + } + + private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + return null; + } + + if (!$nameScope->hasTypeAlias($aliasName)) { + return null; + } + + $className = $nameScope->getClassNameForTypeAlias(); + if ($className === null) { + return null; + } + + $aliasNameInClassScope = $className . '::' . $aliasName; + + if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { + return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; + } + + // prevent infinite recursion + if (array_key_exists($className, $this->resolvingClassTypeAliases)) { + return null; + } + + $this->resolvingClassTypeAliases[$className] = true; + + if (!$this->reflectionProvider->hasClass($className)) { + unset($this->resolvingClassTypeAliases[$className]); + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $localTypeAliases = $classReflection->getTypeAliases(); + + unset($this->resolvingClassTypeAliases[$className]); + + if (!array_key_exists($aliasName, $localTypeAliases)) { + return null; + } + + if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { + // resolve circular reference as ErrorType to make it easier to detect + throw new CircularTypeAliasDefinitionException(); + } + + $this->inProcess[$aliasNameInClassScope] = true; + + try { + $unresolvedAlias = $localTypeAliases[$aliasName]; + $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); + } catch (CircularTypeAliasDefinitionException) { + $resolvedAliasType = new CircularTypeAliasErrorType(); + } + + $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; + unset($this->inProcess[$aliasNameInClassScope]); + + return $resolvedAliasType; + } + + private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (!array_key_exists($aliasName, $this->globalTypeAliases)) { + return null; + } + + if (array_key_exists($aliasName, $this->resolvedGlobalTypeAliases)) { + return $this->resolvedGlobalTypeAliases[$aliasName]; + } + + if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { + throw new ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); + } + + if (array_key_exists($aliasName, $this->inProcess)) { + throw new ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); + } + + $this->inProcess[$aliasName] = true; + + $aliasTypeString = $this->globalTypeAliases[$aliasName]; + $aliasType = $this->typeStringResolver->resolve($aliasTypeString); + $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType; + + unset($this->inProcess[$aliasName]); + + return $aliasType; + } + +} diff --git a/src/Type/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 b4fe73d86e..12b5ecbd15 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -2,7 +2,19 @@ namespace PHPStan\Type; -class VerbosityLevel +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; + +final class VerbosityLevel { private const TYPE_ONLY = 1; @@ -13,55 +25,171 @@ class VerbosityLevel /** @var self[] */ private static array $registry; - private int $value; - - private function __construct(int $value) + /** + * @param self::* $value + */ + private function __construct(private int $value) { - $this->value = $value; } + /** + * @param self::* $value + */ private static function create(int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } + /** @return self::* */ + public function getLevelValue(): int + { + return $this->value; + } + + /** @api */ public static function typeOnly(): self { return self::create(self::TYPE_ONLY); } + /** @api */ public static function value(): self { return self::create(self::VALUE); } + /** @api */ public static function precise(): self { return self::create(self::PRECISE); } + /** @api */ public static function cache(): self { return self::create(self::CACHE); } - public static function getRecommendedLevelByType(Type $type): self + public function isTypeOnly(): bool { - $moreVerbose = false; - TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$moreVerbose): Type { + 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, &$veryVerbose): Type { if ($type->isCallable()->yes()) { $moreVerbose = true; + + // Keep checking if we need to be very verbose. + return $traverse($type); + } + 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 ConstantType && !$type instanceof NullType) { + if ($type instanceof IntegerRangeType) { $moreVerbose = true; return $type; } + return $traverse($type); + }; + + /** @var bool $moreVerbose */ + $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; + TypeTraverser::map($acceptingType, $moreVerboseCallback); + + if ($veryVerbose) { + return self::precise(); + } + + if ($moreVerbose) { + $verbosity = self::value(); + } + + if ($acceptedType === null) { + return $verbosity ?? self::typeOnly(); + } + + $containsInvariantTemplateType = false; + TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$containsInvariantTemplateType): Type { + if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { + $reflection = $type->getClassReflection(); + if ($reflection !== null) { + $templateTypeMap = $reflection->getTemplateTypeMap(); + foreach ($templateTypeMap->getTypes() as $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + + if (!$templateType->getVariance()->invariant()) { + continue; + } + + $containsInvariantTemplateType = true; + return $type; + } + } + } + return $traverse($type); }); - return $moreVerbose ? self::value() : self::typeOnly(); + if (!$containsInvariantTemplateType) { + return $verbosity ?? self::typeOnly(); + } + + /** @var bool $moreVerbose */ + $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; + TypeTraverser::map($acceptedType, $moreVerboseCallback); + + if ($veryVerbose) { + return self::precise(); + } + + return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly(); } /** @@ -69,13 +197,12 @@ public static function getRecommendedLevelByType(Type $type): self * @param callable(): string $valueCallback * @param callable(): string|null $preciseCallback * @param callable(): string|null $cacheCallback - * @return string */ public function handle( callable $typeOnlyCallback, callable $valueCallback, ?callable $preciseCallback = null, - ?callable $cacheCallback = null + ?callable $cacheCallback = null, ): string { if ($this->value === self::TYPE_ONLY) { @@ -94,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 \PHPStan\ShouldNotHappenException(); + return $valueCallback(); } } diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 6224878cc9..83c5a05726 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -2,48 +2,76 @@ 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; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; +use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +/** @api */ class VoidType implements Type { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use NonOffsetAccessibleTypeTrait; use FalseyBooleanTypeTrait; use NonGenericTypeTrait; + use UndecidedComparisonTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; + + /** @api */ + 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 { - return TrinaryLogic::createFromBoolean($type instanceof self); + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + 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 @@ -61,6 +89,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -81,23 +114,159 @@ 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(); + } + + 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 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 - * @return Type - */ - 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 @@ + * @implements ArrayAccess @@ -88,10 +88,11 @@ class ArrayObject implements IteratorAggregate, ArrayAccess /** * @template TValue - * @implements Iterator - * @implements ArrayAccess + * @implements Iterator + * @implements IteratorAggregate + * @implements ArrayAccess */ -class SplFixedArray implements Iterator, ArrayAccess, Countable +class SplFixedArray implements Iterator, IteratorAggregate, ArrayAccess, Countable { /** * @template TInput @@ -101,7 +102,7 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable public static function fromArray(array $array, bool $save_indexes = true): SplFixedArray { } /** - * @return array + * @return array */ public function toArray(): array { } } diff --git a/stubs/Countable.stub b/stubs/Countable.stub new file mode 100644 index 0000000000..2491db1ce5 --- /dev/null +++ b/stubs/Countable.stub @@ -0,0 +1,9 @@ +',args?:mixed[],object?:object}> + * @throws void + */ + public function getTrace(); + + /** + * @return string + * @throws void + */ + public function getTraceAsString(); + + /** + * @return null|Throwable + * @throws void + */ + public function getPrevious(); + + /** + * @return string + */ + public function __toString(); +} + +class Exception implements Throwable +{ + + /** + * @return string + * @throws void + */ + final public function getMessage(): string {} + + /** + * @return mixed + * @throws void + */ + final public function getCode() {} + + /** + * @return string + * @throws void + */ + final public function getFile(): string {} + + /** + * @return int + * @throws void + */ + final public function getLine(): int {} + + /** + * @return list',args?:mixed[],object?:object}> + * @throws void + */ + final public function getTrace(): array {} + + /** + * @return null|Throwable + * @throws void + */ + final public function getPrevious(): ?Throwable {} + + /** + * @return string + * @throws void + */ + final public function getTraceAsString(): string {} + +} + +class Error implements Throwable +{ + + /** + * @return string + * @throws void + */ + final public function getMessage(): string {} + + /** + * @return mixed + * @throws void + */ + final public function getCode() {} + + /** + * @return string + * @throws void + */ + final public function getFile(): string {} + + /** + * @return int + * @throws void + */ + final public function getLine(): int {} + + /** + * @return list',args?:mixed[],object?:object}> + * @throws void + */ + final public function getTrace(): array {} + + /** + * @return null|Throwable + * @throws void + */ + final public function getPrevious(): ?Throwable {} + + /** + * @return string + * @throws void + */ + final public function getTraceAsString(): string {} + +} diff --git a/stubs/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 5c63cbce7d..46d0be7c5d 100644 --- a/stubs/PDOStatement.stub +++ b/stubs/PDOStatement.stub @@ -2,9 +2,21 @@ /** * @implements Traversable> + * @implements IteratorAggregate> * @link https://php.net/manual/en/class.pdostatement.php */ -class PDOStatement implements Traversable +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/ReflectionAttribute.stub b/stubs/ReflectionAttribute.stub new file mode 100644 index 0000000000..0bac59de5b --- /dev/null +++ b/stubs/ReflectionAttribute.stub @@ -0,0 +1,21 @@ + + */ + public function getName() : string + { + } + + /** + * @return T + */ + public function newInstance() : object + { + } +} diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index 2594241e67..06f8e08a2e 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -7,12 +7,14 @@ class ReflectionClass { /** + * @readonly * @var class-string */ public $name; /** * @param T|class-string $argument + * @throws ReflectionException */ public function __construct($argument) {} @@ -29,7 +31,7 @@ class ReflectionClass public function newInstance(...$args) {} /** - * @param array $args + * @param array $args * * @return T */ @@ -40,4 +42,10 @@ class ReflectionClass */ public function newInstanceWithoutConstructor(); + /** + * @return list> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } } diff --git a/stubs/ReflectionClassConstant.stub b/stubs/ReflectionClassConstant.stub new file mode 100644 index 0000000000..4396980e06 --- /dev/null +++ b/stubs/ReflectionClassConstant.stub @@ -0,0 +1,11 @@ +> + */ + 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 c096d0f608..0154996741 100644 --- a/stubs/ReflectionFunctionAbstract.stub +++ b/stubs/ReflectionFunctionAbstract.stub @@ -8,4 +8,10 @@ abstract class ReflectionFunctionAbstract */ public function getFileName () {} + /** + * @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 @@ +> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } +} diff --git a/stubs/ReflectionProperty.stub b/stubs/ReflectionProperty.stub new file mode 100644 index 0000000000..002daeeeee --- /dev/null +++ b/stubs/ReflectionProperty.stub @@ -0,0 +1,11 @@ +> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } +} diff --git a/stubs/SplObjectStorage.stub b/stubs/SplObjectStorage.stub new file mode 100644 index 0000000000..146785e522 --- /dev/null +++ b/stubs/SplObjectStorage.stub @@ -0,0 +1,66 @@ + + * @template-implements SeekableIterator + * @template-implements ArrayAccess + */ +class SplObjectStorage implements Countable, Iterator, SeekableIterator, Serializable, ArrayAccess +{ + + /** + * @param \SplObjectStorage $storage + */ + public function addAll(SplObjectStorage $storage): void { } + + /** + * @param TObject $object + * @param TData $data + */ + public function attach(object $object, $data = null): void { } + + /** + * @param TObject $object + */ + public function contains(object $object): bool { } + + /** + * @param TObject $object + */ + public function detach(object $object): void { } + + /** + * @param TObject $object + */ + public function getHash(object $object): string { } + + /** + * @return TData + */ + public function getInfo() { } + + /** + * @param \SplObjectStorage<*, *> $storage + */ + public function removeAll(SplObjectStorage $storage): void { } + + /** + * @param \SplObjectStorage<*, *> $storage + */ + public function removeAllExcept(SplObjectStorage $storage): void { } + + /** + * @param TData $data + */ + public function setInfo($data): void { } + + /** + * @param TObject $offset + * @return TData + */ + public function offsetGet($offset); + +} diff --git a/stubs/WeakReference.stub b/stubs/WeakReference.stub index 7a71b77065..060dfe1a8d 100644 --- a/stubs/WeakReference.stub +++ b/stubs/WeakReference.stub @@ -16,3 +16,19 @@ final class WeakReference /** @return ?T */ public function get() {} } + + +/** + * @template TKey of object + * @template TValue + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +final class WeakMap implements \ArrayAccess, \Countable, \IteratorAggregate +{ + /** + * @param TKey $offset + * @return TValue + */ + public function offsetGet($offset) {} +} diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub new file mode 100644 index 0000000000..3297e47316 --- /dev/null +++ b/stubs/arrayFunctions.stub @@ -0,0 +1,220 @@ + $one + * @param callable(TReturn, TIn): TReturn $two + * @param TReturn $three + * + * @return TReturn + */ +function array_reduce( + array $one, + callable $two, + $three = null +) {} + +/** + * @template T of mixed + * + * @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 + * + * @param TArray $array + * @param callable(T,T):int $callback + */ +function uasort(array &$array, callable $callback): bool +{} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + * @param callable(T,T):int $callback + */ +function usort(array &$array, callable $callback): bool +{} + +/** + * @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 array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_uintersect_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( + 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 038d0fcb79..e58c7f0682 100644 --- a/stubs/date.stub +++ b/stubs/date.stub @@ -1,11 +1,36 @@ + * @implements \Traversable + */ +class DatePeriod implements \IteratorAggregate, \Traversable { /** - * @var int|false + * @return TEnd */ - public $days; + public function getEndDate() + { + } + + /** + * @return TRecurrences + */ + public function getRecurrences() + { + + } + + /** + * @return TDate + */ + public function getStartDate(): DateTimeInterface + { + + } } 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 99066ac971..e05628d968 100644 --- a/stubs/ext-ds.stub +++ b/stubs/ext-ds.stub @@ -2,24 +2,25 @@ namespace Ds; +use ArrayAccess; 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(): Collection; + public function copy(); /** * @return array @@ -43,7 +44,7 @@ final class Deque implements Sequence /** * @return Deque */ - public function copy(): Deque + public function copy() { } @@ -60,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 { } @@ -92,8 +93,9 @@ final class Deque implements Sequence * @template TKey * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Map implements Collection +final class Map implements Collection, ArrayAccess { /** * @param iterable $values @@ -188,7 +190,7 @@ final class Map implements Collection * @param (callable(TKey, TValue): bool)|null $callback * @return Map */ - public function filter(callable $callback = null): Map + public function filter(?callable $callback = null): Map { } @@ -282,7 +284,7 @@ final class Map implements Collection * @param (callable(TValue, TValue): int)|null $comparator * @return void */ - public function sort(callable $comparator = null) + public function sort(?callable $comparator = null) { } @@ -290,7 +292,7 @@ final class Map implements Collection * @param (callable(TValue, TValue): int)|null $comparator * @return Map */ - public function sorted(callable $comparator = null): Map + public function sorted(?callable $comparator = null): Map { } @@ -298,7 +300,7 @@ final class Map implements Collection * @param (callable(TKey, TKey): int)|null $comparator * @return void */ - public function ksort(callable $comparator = null) + public function ksort(?callable $comparator = null) { } @@ -306,7 +308,7 @@ final class Map implements Collection * @param (callable(TKey, TKey): int)|null $comparator * @return Map */ - public function ksorted(callable $comparator = null): Map + public function ksorted(?callable $comparator = null): Map { } @@ -346,8 +348,8 @@ final class Map implements Collection } /** - * @template-covariant TKey - * @template-covariant TValue + * @template TKey + * @template TValue */ final class Pair implements JsonSerializable { @@ -380,8 +382,9 @@ final class Pair implements JsonSerializable /** * @template TValue * @extends Collection + * @extends ArrayAccess */ -interface Sequence extends Collection +interface Sequence extends Collection, ArrayAccess { /** * @param callable(TValue): TValue $callback @@ -398,7 +401,7 @@ interface Sequence extends Collection * @param (callable(TValue): bool)|null $callback * @return Sequence */ - public function filter(callable $callback = null): Sequence; + public function filter(?callable $callback = null); /** * @param TValue $value @@ -429,7 +432,7 @@ interface Sequence extends Collection * @param string $glue * @return string */ - public function join(string $glue = null): string; + public function join(?string $glue = null): string; /** * @return TValue @@ -442,18 +445,19 @@ interface Sequence extends Collection * @param callable(TValue): TNewValue $callback * @return Sequence */ - public function map(callable $callback): Sequence; + public function map(callable $callback); /** * @template TValue2 * @param iterable $values * @return Sequence */ - public function merge(iterable $values): Sequence; + public function merge(iterable $values); /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function pop(); @@ -480,7 +484,7 @@ interface Sequence extends Collection /** * @return Sequence */ - public function reversed(): Sequence; + public function reversed(); /** * @param TValue $value @@ -492,25 +496,26 @@ interface Sequence extends Collection /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function shift(); /** * @return Sequence */ - public function slice(int $index, ?int $length = null): Sequence; + public function slice(int $index, ?int $length = null); /** * @param (callable(TValue, TValue): int)|null $comparator * @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): Sequence; + public function sorted(?callable $comparator = null); /** * @param TValue ...$values @@ -536,7 +541,7 @@ final class Vector implements Sequence /** * @return Vector */ - public function copy(): Vector + public function copy() { } @@ -558,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 { } @@ -566,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 { } @@ -592,8 +597,9 @@ final class Vector implements Sequence /** * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Set implements Collection +final class Set implements Collection, ArrayAccess { /** * @param iterable $values @@ -636,7 +642,7 @@ final class Set implements Collection * @param (callable(TValue): bool)|null $callback * @return Set */ - public function filter(callable $callback = null): Set + public function filter(?callable $callback = null): Set { } @@ -673,6 +679,15 @@ final class Set implements Collection { } + /** + * @template TNewValue + * @param callable(TValue): TNewValue $callback + * @return Set + */ + public function map(callable $callback): Set + { + } + /** * @template TValue2 * @param iterable $values @@ -682,6 +697,16 @@ final class Set implements Collection { } + /** + * @template TCarry + * @param callable(TCarry, TValue): TCarry $callback + * @param TCarry $initial + * @return TCarry + */ + public function reduce(callable $callback, $initial = null) + { + } + /** * @param TValue ...$values */ @@ -706,7 +731,7 @@ final class Set implements Collection /** * @param (callable(TValue, TValue): int)|null $comparator */ - public function sort(callable $comparator = null): void + public function sort(?callable $comparator = null): void { } @@ -714,7 +739,7 @@ final class Set implements Collection * @param (callable(TValue, TValue): int)|null $comparator * @return Set */ - public function sorted(callable $comparator = null): Set + public function sorted(?callable $comparator = null): Set { } @@ -747,8 +772,9 @@ final class Set implements Collection /** * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Stack implements Collection +final class Stack implements Collection, ArrayAccess { /** * @param iterable $values @@ -775,6 +801,7 @@ final class Stack implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -799,8 +826,9 @@ final class Stack implements Collection /** * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Queue implements Collection +final class Queue implements Collection, ArrayAccess { /** * @param iterable $values @@ -827,6 +855,7 @@ final class Queue implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -871,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 @@ + + */ +interface RecursiveIterator extends Iterator +{ + +} + /** * @template-covariant TKey * @template-covariant TValue @@ -70,12 +81,24 @@ class Generator implements Iterator } /** - * @implements Traversable - * @implements ArrayAccess + * @implements Traversable + * @implements ArrayAccess + * @implements Iterator + * @implements RecursiveIterator */ -class SimpleXMLElement implements Traversable, ArrayAccess +class SimpleXMLElement implements Traversable, ArrayAccess, Iterator, RecursiveIterator { + /** + * @return ($filename is null ? string|false : bool) + */ + public function asXML(?string $filename = null) { } + + /** + * @return ($filename is null ? string|false : bool) + */ + public function saveXML(?string $filename = null) { } + } /** @@ -89,7 +112,7 @@ interface SeekableIterator extends Iterator } /** - * @template TKey + * @template TKey of array-key * @template TValue * @implements SeekableIterator * @implements ArrayAccess @@ -129,20 +152,17 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Countable } /** - * @template T of \Traversable + * @template T of \RecursiveIterator|\IteratorAggregate * @mixin T */ class RecursiveIteratorIterator { - /** * @param T $iterator - * @param int $mode - * @param int $flags */ public function __construct( $iterator, - $mode = RecursiveIteratorIterator::LEAVES_ONLY, + int $mode = RecursiveIteratorIterator::LEAVES_ONLY, int $flags = 0 ) { @@ -150,3 +170,301 @@ class RecursiveIteratorIterator } } + +/** + * @template-covariant TKey + * @template-covariant TValue + * + * @template-extends Iterator + */ +interface OuterIterator extends Iterator { + /** + * @return Iterator + */ + public function getInnerIterator(); +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @template-implements OuterIterator + * + * @mixin TIterator + */ +class IteratorIterator implements OuterIterator { + /** + * @param TIterator $iterator + */ + public function __construct(Traversable $iterator) {} +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @template-extends IteratorIterator + */ +class FilterIterator extends IteratorIterator +{ + +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @extends FilterIterator + */ +class CallbackFilterIterator extends FilterIterator +{ + +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @extends CallbackFilterIterator + * @implements RecursiveIterator + */ +class RecursiveCallbackFilterIterator extends CallbackFilterIterator implements RecursiveIterator +{ + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return RecursiveCallbackFilterIterator + */ + public function getChildren() {} + +} + +/** + * @template TKey of array-key + * @template TValue + * + * @template-implements RecursiveIterator + * @template-extends ArrayIterator + */ +class RecursiveArrayIterator extends ArrayIterator implements RecursiveIterator { + + /** + * @return RecursiveArrayIterator + */ + public function getChildren() {} + + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return TValue + */ + public function current() {} + + /** + * @return TKey + */ + public function key() {} + + /** + * @param callable(TKey, TKey): int $cmp_function + * @return void + */ + public function uksort($cmp_function) { } +} + +/** + * @template TKey + * @template TValue + * @template TIterator as Iterator + * + * @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 new file mode 100644 index 0000000000..cc88f4f6e1 --- /dev/null +++ b/stubs/mysqli.stub @@ -0,0 +1,84 @@ +|numeric-string + */ + public $affected_rows; +} + +class mysqli_result +{ + /** + * @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; + + /** + * @var int + */ + public $errno; + + /** + * @var list + */ + public $error_list; + + /** + * @var string + */ + public $error; + + /** + * @var 0|positive-int + */ + public $field_count; + + /** + * @var int|string + */ + public $insert_id; + + /** + * @var int<0,max>|numeric-string + */ + public $num_rows; + + /** + * @var 0|positive-int + */ + public $param_count; + + /** + * @var non-empty-string + */ + public $sqlstate; + +} diff --git a/stubs/runtime/Attribute.php b/stubs/runtime/Attribute.php new file mode 100644 index 0000000000..18d7de3da4 --- /dev/null +++ b/stubs/runtime/Attribute.php @@ -0,0 +1,83 @@ +flags = $flags; + } + + } +} + +if (\PHP_VERSION_ID < 80100 && !class_exists('ReturnTypeWillChange', false)) { + #[Attribute(Attribute::TARGET_METHOD)] + 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/BackedEnum.php b/stubs/runtime/Enum/BackedEnum.php new file mode 100644 index 0000000000..c830ea24ff --- /dev/null +++ b/stubs/runtime/Enum/BackedEnum.php @@ -0,0 +1,14 @@ + + */ + public static function cases(): array; + } +} diff --git a/stubs/runtime/ReflectionAttribute.php b/stubs/runtime/ReflectionAttribute.php new file mode 100644 index 0000000000..19fe0a66ff --- /dev/null +++ b/stubs/runtime/ReflectionAttribute.php @@ -0,0 +1,41 @@ +|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 452c94acd2..325f09f20b 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -2,13 +2,21 @@ namespace PHPStan\Analyser; -use PHPStan\Broker\Broker; -use PHPStan\File\FileHelper; +use Bug4288\MyClass; +use Bug4713\Service; +use ExtendingKnownClassWithCheck\Foo; +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 extension_loaded; +use function restore_error_handler; +use function sprintf; use const PHP_VERSION_ID; -use function array_reverse; -class AnalyserIntegrationTest extends \PHPStan\Testing\TestCase +class AnalyserIntegrationTest extends PHPStanTestCase { public function testUndefinedVariableFromAssignErrorHasLine(): void @@ -27,13 +35,13 @@ public function testUndefinedVariableFromAssignErrorHasLine(): void public function testMissingPropertyAndMethod(): void { $errors = $this->runAnalyse(__DIR__ . '/../../notAutoloaded/Foo.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testMissingClassErrorAboutMisconfiguredAutoloader(): void { $errors = $this->runAnalyse(__DIR__ . '/../../notAutoloaded/Bar.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testMissingFunctionErrorAboutMisconfiguredAutoloader(): void @@ -47,16 +55,16 @@ public function testMissingFunctionErrorAboutMisconfiguredAutoloader(): void public function testAnonymousClassWithInheritedConstructor(): void { $errors = $this->runAnalyse(__DIR__ . '/data/anonymous-class-with-inherited-constructor.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testNestedFunctionCallsDoNotCauseExcessiveFunctionNesting(): void { 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->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testExtendingUnknownClass(): void @@ -64,28 +72,23 @@ 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 { $errors = $this->runAnalyse(__DIR__ . '/data/extending-known-class-with-check.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); - $broker = self::getContainer()->getByType(Broker::class); - $this->assertTrue($broker->hasClass(\ExtendingKnownClassWithCheck\Foo::class)); + $reflectionProvider = $this->createReflectionProvider(); + $this->assertTrue($reflectionProvider->hasClass(Foo::class)); } public function testInfiniteRecursionWithCallable(): void { $errors = $this->runAnalyse(__DIR__ . '/data/Foo-callable.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testClassThatExtendsUnknownClassIn3rdPartyPropertyTypeShouldNotCauseAutoloading(): void @@ -122,16 +125,18 @@ public function testCustomFunctionWithNameEquivalentInSignatureMap(): void } require_once __DIR__ . '/data/custom-function-in-signature-map.php'; $errors = $this->runAnalyse(__DIR__ . '/data/custom-function-in-signature-map.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testAnonymousClassWithWrongFilename(): void { $errors = $this->runAnalyse(__DIR__ . '/data/anonymous-class-wrong-filename-regression.php'); $this->assertCount(5, $errors); - $this->assertStringContainsString('Return typehint of method', $errors[0]->getMessage()); + $this->assertStringContainsString('Method', $errors[0]->getMessage()); + $this->assertStringContainsString('has invalid return type', $errors[0]->getMessage()); $this->assertSame(16, $errors[0]->getLine()); - $this->assertStringContainsString('Return typehint of method', $errors[1]->getMessage()); + $this->assertStringContainsString('Method', $errors[1]->getMessage()); + $this->assertStringContainsString('has invalid return type', $errors[1]->getMessage()); $this->assertSame(16, $errors[1]->getLine()); $this->assertSame('Instantiated class AnonymousClassWrongFilename\Bar not found.', $errors[2]->getMessage()); $this->assertSame(18, $errors[2]->getLine()); @@ -145,13 +150,19 @@ public function testAnonymousClassWithWrongFilename(): void public function testExtendsPdoStatementCrash(): void { $errors = $this->runAnalyse(__DIR__ . '/data/extends-pdo-statement.php'); - $this->assertCount(0, $errors); + $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'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testNestedNamespaces(): void @@ -160,55 +171,42 @@ public function testNestedNamespaces(): void $this->assertCount(2, $errors); $this->assertSame('Property y\x::$baz has unknown class x\baz as its type.', $errors[0]->getMessage()); $this->assertSame(15, $errors[0]->getLine()); - $this->assertSame('Parameter $baz of method y\x::__construct() has invalid typehint type x\baz.', $errors[1]->getMessage()); + $this->assertSame('Parameter $baz of method y\x::__construct() has invalid type x\baz.', $errors[1]->getMessage()); $this->assertSame(16, $errors[1]->getLine()); } public function testClassExistsAutoloadingError(): void { $errors = $this->runAnalyse(__DIR__ . '/data/class-exists.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testCollectWarnings(): void { 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 { $errors = $this->runAnalyse(__DIR__ . '/data/property-assign-intersection-static-type-bug.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug2823(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-2823.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testTwoSameClassesInSingleFile(): void { $errors = $this->runAnalyse(__DIR__ . '/data/two-same-classes.php'); - $this->assertCount(4, $errors); + $this->assertCount(5, $errors); + $error = $errors[0]; $this->assertSame('Property TwoSame\Foo::$prop (string) does not accept default value of type int.', $error->getMessage()); $this->assertSame(9, $error->getLine()); @@ -218,47 +216,1401 @@ public function testTwoSameClassesInSingleFile(): void $this->assertSame(13, $error->getLine()); $error = $errors[2]; - $this->assertSame('Property TwoSame\Foo::$prop (int) does not accept default value of type string.', $error->getMessage()); - $this->assertSame(25, $error->getLine()); + $this->assertSame('If condition is always false.', $error->getMessage()); + $this->assertSame(26, $error->getLine()); $error = $errors[3]; + $this->assertSame('Property TwoSame\Foo::$prop (int) does not accept default value of type string.', $error->getMessage()); + $this->assertSame(33, $error->getLine()); + + $error = $errors[4]; $this->assertSame('Property TwoSame\Foo::$prop2 (int) does not accept default value of type string.', $error->getMessage()); - $this->assertSame(28, $error->getLine()); + $this->assertSame(36, $error->getLine()); + } + + public function 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->assertCount(0, $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 { $errors = $this->runAnalyse(__DIR__ . '/../Rules/Methods/data/bug-3415.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3415Two(): void { $errors = $this->runAnalyse(__DIR__ . '/../Rules/Methods/data/bug-3415-2.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } - /** - * @param string $file - * @return \PHPStan\Analyser\Error[] - */ - private function runAnalyse(string $file): array + public function testBug3468(): void { - $file = $this->getFileHelper()->normalizePath($file); - /** @var \PHPStan\Analyser\Analyser $analyser */ + $errors = $this->runAnalyse(__DIR__ . '/data/bug-3468.php'); + $this->assertNoErrors($errors); + } + + public function testBug3686(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-3686.php'); + $this->assertNoErrors($errors); + } + + public function testBug3379(): void + { + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3379.php'); + $this->assertCount(1, $errors); + $this->assertSame('Constant SOME_UNKNOWN_CONST not found.', $errors[0]->getMessage()); + } + + public function testBug3798(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-3798.php'); + $this->assertNoErrors($errors); + } + + public function testBug3909(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-3909.php'); + $this->assertNoErrors($errors); + } + + public function testBug4097(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4097.php'); + $this->assertNoErrors($errors); + } + + public function testBug4300(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4300.php'); + $this->assertCount(1, $errors); + $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[0]->getMessage()); + $this->assertSame(13, $errors[0]->getLine()); + } + + public function testBug4513(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4513.php'); + $this->assertNoErrors($errors); + } + + public function testBug1871(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-1871.php'); + $this->assertNoErrors($errors); + } + + public function testBug3309(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-3309.php'); + $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'; + $errors = $this->runAnalyse(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); + $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'); + $this->assertNoErrors($errors); + } + + public function testBug1843(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-1843.php'); + $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'); + $this->assertCount(1, $errors); + $this->assertSame('Method Bug4713\Service::createInstance() should return Bug4713\Service but returns object.', $errors[0]->getMessage()); + + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(Service::class); + $parameter = $class->getNativeMethod('createInstance')->getOnlyVariant()->getParameters()[0]; + $defaultValue = $parameter->getDefaultValue(); + $this->assertInstanceOf(ConstantStringType::class, $defaultValue); + $this->assertSame(Service::class, $defaultValue->getValue()); + } + + public function testBug4288(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4288.php'); + $this->assertNoErrors($errors); + + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(MyClass::class); + $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'); + $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 + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4702.php'); + $this->assertNoErrors($errors); + } + + public function testFunctionThatExistsOn72AndLater(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/ldap-exop-passwd.php'); + if (PHP_VERSION_ID < 80100) { + $this->assertNoErrors($errors); + return; + } + + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $ldap of function ldap_exop_passwd expects LDAP\Connection, resource given.', $errors[0]->getMessage()); + } + + public function testBug4715(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4715.php'); + $this->assertNoErrors($errors); + } + + public function testBug4734(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); + $this->assertCount(3, $errors); + + $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[0]->getMessage()); + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[1]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[2]->getMessage()); + } + + public function testBug5231(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5231.php'); + $this->assertNotEmpty($errors); + } + + public function testBug5231Two(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5231_2.php'); + $this->assertNotEmpty($errors); + } + + public function 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__ . '/nsrt/bug-5529.php'); + $this->assertNoErrors($errors); + } + + public function testBug5527(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5527.php'); + $this->assertNoErrors($errors); + } + + public function testBug5639(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5639.php'); + $this->assertNoErrors($errors); + } + + public function testBug5657(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5657.php'); + $this->assertNoErrors($errors); + } + + public function testBug5951(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5951.php'); + $this->assertNoErrors($errors); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/enums-integration.php'); + $this->assertCount(3, $errors); + $this->assertSame('Access to an undefined property EnumIntegrationTest\Foo::TWO::$value.', $errors[0]->getMessage()); + $this->assertSame(22, $errors[0]->getLine()); + $this->assertSame('Access to undefined constant EnumIntegrationTest\Bar::NONEXISTENT.', $errors[1]->getMessage()); + $this->assertSame(49, $errors[1]->getLine()); + $this->assertSame('Strict comparison using === between EnumIntegrationTest\Foo::ONE and EnumIntegrationTest\Foo::TWO will always evaluate to false.', $errors[2]->getMessage()); + $this->assertSame(79, $errors[2]->getLine()); + } + + public function testBug6255(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6255.php'); + $this->assertNoErrors($errors); + } + + public function testBug6300(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6300.php'); + $this->assertCount(2, $errors); + $this->assertSame('Call to an undefined method Bug6300\Bar::get().', $errors[0]->getMessage()); + $this->assertSame(23, $errors[0]->getLine()); + + $this->assertSame('Access to an undefined property Bug6300\Bar::$fooProp.', $errors[1]->getMessage()); + $this->assertSame(24, $errors[1]->getLine()); + } + + public function testBug6466(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6466.php'); + $this->assertNoErrors($errors); + } + + public function 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( + __DIR__ . '/data/bug-6253.php', + [ + __DIR__ . '/data/bug-6253.php', + __DIR__ . '/data/bug-6253-app-scope-trait.php', + __DIR__ . '/data/bug-6253-collection-trait.php', + ], + ); + $this->assertNoErrors($errors); + } + + public function testBug6442(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6442.php'); + $this->assertCount(2, $errors); + $this->assertSame('Dumped type: \'Bug6442\\\B\'', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame('Dumped type: \'Bug6442\\\A\'', $errors[1]->getMessage()); + $this->assertSame(9, $errors[1]->getLine()); + } + + public function testBug6375(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6375.php'); + $this->assertNoErrors($errors); + } + + public function testBug6501(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6501.php'); + $this->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 + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6114.php'); + $this->assertNoErrors($errors); + } + + public function testBug6681(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6681.php'); + $this->assertNoErrors($errors); + } + + public function testBug6212(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6212.php'); + $this->assertNoErrors($errors); + } + + public function testBug6740(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6740-b.php'); + $this->assertNoErrors($errors); + } + + public function testBug6866(): void + { + $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 \PHPStan\File\FileHelper $fileHelper */ - $fileHelper = self::getContainer()->getByType(FileHelper::class); - /** @var \PHPStan\Analyser\Error[] $errors */ - $errors = $analyser->analyse([$file])->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 3443d5b956..1b41db9157 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -2,22 +2,47 @@ namespace PHPStan\Analyser; -use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Cache\Cache; -use PHPStan\Command\IgnoredRegexValidator; +use Nette\DI\Container; +use PhpParser\Lexer; +use PhpParser\NodeVisitor\NameResolver; +use PhpParser\Parser\Php7; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\Ignore\IgnoreLexer; +use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; -use PHPStan\File\RelativePathHelper; -use PHPStan\Parser\DirectParser; +use PHPStan\Dependency\ExportedNodeResolver; +use PHPStan\DependencyInjection\Nette\NetteContainer; +use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +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\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; - -use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; +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; - -class AnalyserTest extends \PHPStan\Testing\TestCase +use stdClass; +use function array_map; +use function array_merge; +use function assert; +use function count; +use function is_string; +use function sprintf; +use function str_replace; +use function strtoupper; +use function substr; +use const PHP_OS; + +class AnalyserTest extends PHPStanTestCase { public function testReturnErrorIfIgnoredMessagesDoesNotOccur(): void @@ -40,20 +65,56 @@ public function testDoNotReturnErrorIfIgnoredMessagesDoNotOccurWhileAnalysingInd $this->assertEmpty($result); } - public function testReportInvalidIgnorePatternEarly(): void + public function testFileWithAnIgnoredError(): void { - $result = $this->runAnalyser(['#Regexp syntax error'], true, __DIR__ . '/data/parse-error.php', false); - $this->assertSame([ - "No ending delimiter '#' found in pattern: #Regexp syntax error", - ], $result); + $result = $this->runAnalyser(['#Fail\.#'], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); } - public function testFileWithAnIgnoredError(): void + public function testFileWithAnIgnoredErrorMessage(): void { - $result = $this->runAnalyser(['#Fail\.#'], true, __DIR__ . '/data/bootstrap-error.php', false); + $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(); @@ -61,7 +122,7 @@ public function testIgnoringBrokenConfigurationDoesNotWork(): void $this->assertCount(2, $result); assert($result[0] instanceof Error); $this->assertSame('Class PHPStan\Tests\Baz was not found while trying to analyse it - autoloading is probably not configured properly.', $result[0]->getMessage()); - $this->assertSame('Error message "Class PHPStan\Tests\Baz was not found while trying to analyse it - autoloading is probably not configured properly." cannot be ignored, use excludes_analyse instead.', $result[1]); + $this->assertSame('Error message "Class PHPStan\Tests\Baz was not found while trying to analyse it - autoloading is probably not configured properly." cannot be ignored, use excludePaths instead.', $result[1]); } public function testIgnoreErrorByPath(): void @@ -73,20 +134,74 @@ public function testIgnoreErrorByPath(): void ], ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } - public function testIgnoreErrorByPathAndCount(): void + 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->assertCount(0, $result); + $this->assertNoErrors($result); } public function dataTrueAndFalse(): array @@ -99,7 +214,32 @@ public function dataTrueAndFalse(): array /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles + */ + 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 */ public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): void { @@ -131,7 +271,6 @@ public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testIgnoreErrorByPathAndCountLessThanExpected(bool $onlyFiles): void { @@ -192,7 +331,22 @@ public function testIgnoreErrorByPaths(): void ], ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); - $this->assertCount(0, $result); + $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 @@ -225,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 = [ @@ -238,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 [ @@ -252,7 +436,6 @@ public function dataIgnoreErrorInTraitUsingClassFilePath(): array /** * @dataProvider dataIgnoreErrorInTraitUsingClassFilePath - * @param string $pathToIgnore */ public function testIgnoreErrorInTraitUsingClassFilePath(string $pathToIgnore): void { @@ -266,7 +449,7 @@ public function testIgnoreErrorInTraitUsingClassFilePath(string $pathToIgnore): __DIR__ . '/data/traits-ignore/Foo.php', __DIR__ . '/data/traits-ignore/FooTrait.php', ], true); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } public function testIgnoredErrorMissingMessage(): void @@ -285,32 +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]); - } - - public function testIgnoredErrorMessageStillValidatedIfMissingAPath(): void - { - $ignoreErrors = [ - [ - 'message' => '#Fail\.', - ], - ]; - $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/empty/empty.php', false); - $this->assertCount(2, $result); - $this->assertSame('Ignored error {"message":"#Fail\\\\."} is missing a path.', $result[0]); - $this->assertSame('No ending delimiter \'#\' found in pattern: #Fail\.', $result[1]); + $this->assertSame('Ignored error {"path":"' . $expectedPath . '/data/empty/empty.php"} is missing a message or an identifier.', $result[0]); } public function testReportMultipleParserErrorsAtOnce(): void @@ -331,7 +489,6 @@ public function testReportMultipleParserErrorsAtOnce(): void /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalysed(bool $onlyFiles): void { @@ -348,12 +505,11 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalyse $result = $this->runAnalyser($ignoreErrors, true, [ __DIR__ . '/data/two-fails.php', ], $onlyFiles); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasNotAnalysed(bool $onlyFiles): void { @@ -372,39 +528,39 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasN $result = $this->runAnalyser($ignoreErrors, true, [ __DIR__ . '/data/two-fails.php', ], $onlyFiles); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } - /** - * @dataProvider dataTrueAndFalse - * @param bool $reportUnmatchedIgnoredErrors - */ - public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void + 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()); } /** * @dataProvider dataTrueAndFalse - * @param bool $reportUnmatchedIgnoredErrors */ public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void { @@ -429,105 +585,180 @@ 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 bool $reportUnmatchedIgnoredErrors * @param string|string[] $filePaths - * @param bool $onlyFiles - * @return string[]|\PHPStan\Analyser\Error[] + * @return string[]|Error[] */ private function runAnalyser( array $ignoreErrors, bool $reportUnmatchedIgnoredErrors, $filePaths, - bool $onlyFiles + bool $onlyFiles, ): array { - $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors); + $analyser = $this->createAnalyser(); if (is_string($filePaths)) { $filePaths = [$filePaths]; } $ignoredErrorHelper = new IgnoredErrorHelper( - self::getContainer()->getByType(IgnoredRegexValidator::class), $this->getFileHelper(), $ignoreErrors, - $reportUnmatchedIgnoredErrors + $reportUnmatchedIgnoredErrors, ); $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { return $ignoredErrorHelperResult->getErrors(); } - $normalizedFilePaths = array_map(function (string $path): string { - return $this->getFileHelper()->normalizePath($path); - }, $filePaths); + $normalizedFilePaths = array_map(fn (string $path): string => $this->getFileHelper()->normalizePath($path), $filePaths); $analyserResult = $analyser->analyse($normalizedFilePaths); - $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, - $ignoredErrorHelperResult->getWarnings(), - $analyserResult->getInternalErrors() + array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()), ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): \PHPStan\Analyser\Analyser + private function createAnalyser(): Analyser { - $registry = new Registry([ + $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), ]); + $collectorRegistry = new CollectorRegistry([]); - $traverser = new \PhpParser\NodeTraverser(); - $traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver()); - - $broker = $this->createBroker(); - $printer = new \PhpParser\PrettyPrinter\Standard(); + $reflectionProvider = $this->createReflectionProvider(); $fileHelper = $this->getFileHelper(); - /** @var RelativePathHelper $relativePathHelper */ - $relativePathHelper = self::getContainer()->getService('simpleRelativePathHelper'); - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - $typeSpecifier = $this->createTypeSpecifier($printer, $broker); - $fileTypeMapper = new FileTypeMapper(new DirectReflectionProviderProvider($broker), $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), new AnonymousClassNameHelper($fileHelper, $relativePathHelper)); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); + $typeSpecifier = self::getContainer()->getService('typeSpecifier'); + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); + $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper, self::getContainer()->getByType(StubPhpDocProvider::class)); $nodeScopeResolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), + $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getReflector(), + 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, - false, + 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(); $fileAnalyser = new FileAnalyser( - $this->createScopeFactory($broker, $typeSpecifier), + $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, - new DirectParser(new \PhpParser\Parser\Php7(new \PhpParser\Lexer()), $traverser), - new DependencyResolver($broker), - $fileHelper, - $reportUnmatchedIgnoredErrors + new RichParser( + new Php7($lexer), + new NameResolver(), + self::getContainer(), + new IgnoreLexer(), + ), + 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 + 50, ); } diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index dad3508986..023d00e9f4 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -3,12 +3,18 @@ namespace PHPStan\Analyser; use PHPStan\File\FileHelper; - -class AnalyserTraitsIntegrationTest extends \PHPStan\Testing\TestCase +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 { - /** @var \PHPStan\File\FileHelper */ - private $fileHelper; + private FileHelper $fileHelper; protected function setUp(): void { @@ -35,7 +41,7 @@ public function testMethodDoesNotExist(): void $this->assertSame('Call to an undefined method AnalyseTraits\Bar::doFoo().', $error->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\Bar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/FooTrait.php')), - $error->getFile() + $error->getFile(), ); $this->assertSame(10, $error->getLine()); } @@ -52,7 +58,7 @@ public function testNestedTraits(): void $this->assertSame('Call to an undefined method AnalyseTraits\NestedBar::doFoo().', $firstError->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\NestedBar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/FooTrait.php')), - $firstError->getFile() + $firstError->getFile(), ); $this->assertSame(10, $firstError->getLine()); @@ -60,7 +66,7 @@ public function testNestedTraits(): void $this->assertSame('Call to an undefined method AnalyseTraits\NestedBar::doNestedFoo().', $secondError->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\NestedBar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/NestedFooTrait.php')), - $secondError->getFile() + $secondError->getFile(), ); $this->assertSame(12, $secondError->getLine()); } @@ -100,7 +106,7 @@ public function testTraitInAnonymousClass(): void [ __DIR__ . '/traits/AnonymousClassUsingTrait.php', __DIR__ . '/traits/TraitWithTypeSpecification.php', - ] + ], ); $this->assertCount(1, $errors); $this->assertStringContainsString('Access to an undefined property', $errors[0]->getMessage()); @@ -110,7 +116,7 @@ public function testTraitInAnonymousClass(): void public function testDuplicateMethodDefinition(): void { $errors = $this->runAnalyse([__DIR__ . '/traits/duplicateMethod/Lesson.php']); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testWrongPropertyType(): void @@ -120,14 +126,14 @@ public function testWrongPropertyType(): void $this->assertSame(15, $errors[0]->getLine()); $this->assertSame( $this->fileHelper->normalizePath(__DIR__ . '/traits/wrongProperty/Foo.php'), - $errors[0]->getFile() + $errors[0]->getFile(), ); $this->assertSame('Property TraitsWrongProperty\Foo::$id (int) does not accept string.', $errors[0]->getMessage()); $this->assertSame(17, $errors[1]->getLine()); $this->assertSame( $this->fileHelper->normalizePath(__DIR__ . '/traits/wrongProperty/Foo.php'), - $errors[1]->getFile() + $errors[1]->getFile(), ); $this->assertSame('Property TraitsWrongProperty\Foo::$bar (Ipsum) does not accept int.', $errors[1]->getMessage()); } @@ -145,13 +151,13 @@ public function testReturnThis(): void public function testTraitInEval(): void { $errors = $this->runAnalyse([__DIR__ . '/traits/TraitInEvalUse.php']); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testParameterNotFoundCrash(): void { $errors = $this->runAnalyse([__DIR__ . '/traits/parameter-not-found.php']); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testMissingReturnInAbstractTraitMethod(): void @@ -160,23 +166,63 @@ public function testMissingReturnInAbstractTraitMethod(): void __DIR__ . '/traits/TraitWithAbstractMethod.php', __DIR__ . '/traits/ClassImplementingTraitWithAbstractMethod.php', ]); - $this->assertCount(0, $errors); + $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 \PHPStan\Analyser\Error[] + * @return Error[] */ private function runAnalyse(array $files): array { - $files = array_map(function (string $file): string { - return $this->getFileHelper()->normalizePath($file); - }, $files); - /** @var \PHPStan\Analyser\Analyser $analyser */ + $files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files); + /** @var Analyser $analyser */ $analyser = self::getContainer()->getByType(Analyser::class); - /** @var \PHPStan\Analyser\Error[] $errors */ - $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 a4cf2bddc6..2c4f09e0eb 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRule.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRule.php @@ -4,18 +4,19 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; +use PHPStan\Broker\ClassNotFoundException; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +/** + * @implements Rule + */ class AnonymousClassNameRule implements Rule { - /** @var ReflectionProvider */ - private $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -23,11 +24,6 @@ public function getNodeType(): string return Class_::class; } - /** - * @param Class_ $node - * @param Scope $scope - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { $className = isset($node->namespacedName) @@ -35,11 +31,19 @@ public function processNode(Node $node, Scope $scope): array : (string) $node->name; try { $this->reflectionProvider->getClass($className); - } catch (\PHPStan\Broker\ClassNotFoundException $e) { - return ['not found']; + } catch (ClassNotFoundException) { + 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/AnonymousClassNameRuleTest.php b/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php index 2ea1a331d7..c5f5a5aab1 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php @@ -10,8 +10,8 @@ class AnonymousClassNameRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new AnonymousClassNameRule($broker); + $reflectionProvider = $this->createReflectionProvider(); + return new AnonymousClassNameRule($reflectionProvider); } public function testRule(): void diff --git a/tests/PHPStan/Analyser/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/ClassConstantStubFileTest.php b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php new file mode 100644 index 0000000000..00d83693e6 --- /dev/null +++ b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/class-constant-stub-files.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/classConstantStubFiles.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/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 new file mode 100644 index 0000000000..0d5f8a8dc8 --- /dev/null +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -0,0 +1,41 @@ +gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension-named-args-fixture.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__ . '/dynamic-throw-type-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php new file mode 100644 index 0000000000..7e5fad5dda --- /dev/null +++ b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php @@ -0,0 +1,44 @@ +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'); + } + + /** + * @dataProvider dataAsserts + * @param mixed ...$args + */ + public function testAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/dynamic-return-type.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index d2347ebbfc..7bd49a242a 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Analyser; -class ErrorTest extends \PHPStan\Testing\TestCase +use PHPStan\Testing\PHPStanTestCase; + +class ErrorTest extends PHPStanTestCase { public function testError(): void @@ -13,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 de08902369..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,22 +17,25 @@ public function getNodeType(): string return Node::class; } - /** - * @param Node $node - * @param Scope $scope - * @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 new file mode 100644 index 0000000000..167cad5f8e --- /dev/null +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -0,0 +1,9712 @@ +assertSame('SomeNodeScopeResolverNamespace', $scope->getNamespace()); + $this->assertTrue($scope->isInClass()); + $this->assertSame(Foo::class, $scope->getClassReflection()->getName()); + $this->assertSame('doFoo', $scope->getFunctionName()); + $this->assertSame('$this(SomeNodeScopeResolverNamespace\Foo)', $scope->getVariableType('this')->describe(VerbosityLevel::precise())); + $this->assertTrue($scope->hasVariableType('baz')->yes()); + $this->assertTrue($scope->hasVariableType('lorem')->yes()); + $this->assertFalse($scope->hasVariableType('ipsum')->yes()); + $this->assertTrue($scope->hasVariableType('i')->yes()); + $this->assertTrue($scope->hasVariableType('val')->yes()); + $this->assertSame('SomeNodeScopeResolverNamespace\InvalidArgumentException', $scope->getVariableType('exception')->describe(VerbosityLevel::precise())); + $this->assertTrue($scope->hasVariableType('staticVariable')->yes()); + $this->assertSame($scope->getVariableType('staticVariable')->describe(VerbosityLevel::precise()), 'mixed'); + $this->assertTrue($scope->hasVariableType('staticVariableWithPhpDocType')->yes()); + $this->assertSame($scope->getVariableType('staticVariableWithPhpDocType')->describe(VerbosityLevel::precise()), 'string'); + $this->assertTrue($scope->hasVariableType('staticVariableWithPhpDocType2')->yes()); + $this->assertSame($scope->getVariableType('staticVariableWithPhpDocType2')->describe(VerbosityLevel::precise()), 'int'); + $this->assertTrue($scope->hasVariableType('staticVariableWithPhpDocType3')->yes()); + $this->assertSame($scope->getVariableType('staticVariableWithPhpDocType3')->describe(VerbosityLevel::precise()), 'float'); + }); + } + + private function getFileScope(string $filename): Scope + { + $testScope = null; + $this->processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { + if (!($node instanceof Exit_)) { + return; + } + + $testScope = $scope; + }); + + /** @var Scope */ + return $testScope; + } + + public function dataUnionInCatch(): array + { + return [ + [ + 'CatchUnion\BarException|CatchUnion\FooException', + '$e', + ], + ]; + } + + /** + * @dataProvider dataUnionInCatch + */ + public function testUnionInCatch( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/catch-union.php', + $description, + $expression, + ); + } + + public function dataUnionAndIntersection(): array + { + return [ + [ + 'UnionIntersection\AnotherFoo|UnionIntersection\Foo', + '$this->union->foo', + ], + [ + 'UnionIntersection\Bar', + '$this->union->bar', + ], + [ + 'UnionIntersection\Foo', + '$foo->foo', + ], + [ + '*ERROR*', + '$foo->bar', + ], + [ + 'UnionIntersection\AnotherFoo|UnionIntersection\Foo', + '$this->union->doFoo()', + ], + [ + 'UnionIntersection\Bar', + '$this->union->doBar()', + ], + [ + 'UnionIntersection\Foo', + '$foo->doFoo()', + ], + [ + '*ERROR*', + '$foo->doBar()', + ], + [ + 'UnionIntersection\AnotherFoo&UnionIntersection\Foo', + '$foobar->doFoo()', + ], + [ + 'UnionIntersection\Bar', + '$foobar->doBar()', + ], + [ + '1', + '$this->union::FOO_CONSTANT', + ], + [ + '1', + '$this->union::BAR_CONSTANT', + ], + [ + '1', + '$foo::FOO_CONSTANT', + ], + [ + '*ERROR*', + '$foo::BAR_CONSTANT', + ], + [ + '1', + '$foobar::FOO_CONSTANT', + ], + [ + '1', + '$foobar::BAR_CONSTANT', + ], + [ + '\'foo\'', + 'self::IPSUM_CONSTANT', + ], + [ + 'array{1, 2, 3}', + 'parent::PARENT_CONSTANT', + ], + [ + 'UnionIntersection\Foo', + '$foo::doStaticFoo()', + ], + [ + '*ERROR*', + '$foo::doStaticBar()', + ], + [ + 'UnionIntersection\AnotherFoo&UnionIntersection\Foo', + '$foobar::doStaticFoo()', + ], + [ + 'UnionIntersection\Bar', + '$foobar::doStaticBar()', + ], + [ + 'UnionIntersection\AnotherFoo|UnionIntersection\Foo', + '$this->union::doStaticFoo()', + ], + [ + 'UnionIntersection\Bar', + '$this->union::doStaticBar()', + ], + [ + 'object', + '$this->objectUnion', + ], + [ + 'UnionIntersection\SomeInterface', + '$object', + ], + ]; + } + + /** + * @dataProvider dataUnionAndIntersection + */ + public function testUnionAndIntersection( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/union-intersection.php', + $description, + $expression, + ); + } + + public function dataAssignInIf(): array + { + $testScope = $this->getFileScope(__DIR__ . '/data/if.php'); + + return [ + [ + $testScope, + 'nonexistentVariable', + TrinaryLogic::createNo(), + ], + [ + $testScope, + 'foo', + TrinaryLogic::createMaybe(), + 'bool', // mixed? + ], + [ + $testScope, + 'lorem', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'callParameter', + TrinaryLogic::createYes(), + '3', + ], + [ + $testScope, + 'arrOne', + TrinaryLogic::createYes(), + 'array{\'one\'}', + ], + [ + $testScope, + 'arrTwo', + TrinaryLogic::createYes(), + 'array{test: \'two\', 0: Foo}', + ], + [ + $testScope, + 'arrThree', + TrinaryLogic::createYes(), + 'array{\'three\'}', + ], + [ + $testScope, + 'inArray', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'i', + TrinaryLogic::createYes(), + 'int<0, 4>', + ], + [ + $testScope, + 'f', + TrinaryLogic::createMaybe(), + 'int<1, max>', + ], + [ + $testScope, + 'anotherF', + TrinaryLogic::createYes(), + 'int<1, max>', + ], + [ + $testScope, + 'matches', + TrinaryLogic::createYes(), + 'array{0?: string}', + ], + [ + $testScope, + 'anotherArray', + TrinaryLogic::createYes(), + 'array{test: array{\'another\'}}', + ], + [ + $testScope, + 'ifVar', + TrinaryLogic::createYes(), + '1|2|3', + ], + [ + $testScope, + 'ifNotVar', + TrinaryLogic::createMaybe(), + '1|2', + ], + [ + $testScope, + 'ifNestedVar', + TrinaryLogic::createYes(), + '1|2|3', + ], + [ + $testScope, + 'ifNotNestedVar', + TrinaryLogic::createMaybe(), + '1|2|3', + ], + [ + $testScope, + 'variableOnlyInEarlyTerminatingElse', + TrinaryLogic::createNo(), + ], + [ + $testScope, + 'matches2', + TrinaryLogic::createMaybe(), + 'array{0?: string}', + ], + [ + $testScope, + 'inTry', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'matches3', + TrinaryLogic::createYes(), + 'array{}|array{string}', + ], + [ + $testScope, + 'matches4', + TrinaryLogic::createMaybe(), + 'array{}|array{string}', + ], + [ + $testScope, + 'issetFoo', + TrinaryLogic::createYes(), + 'Foo', + ], + [ + $testScope, + 'issetBar', + TrinaryLogic::createYes(), + 'mixed~null', + ], + [ + $testScope, + 'issetBaz', + TrinaryLogic::createYes(), + 'mixed~null', + ], + [ + $testScope, + 'doWhileVar', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'switchVar', + TrinaryLogic::createYes(), + '1|2|3|4', + ], + [ + $testScope, + 'noSwitchVar', + TrinaryLogic::createMaybe(), + '1', + ], + [ + $testScope, + 'anotherNoSwitchVar', + TrinaryLogic::createMaybe(), + '1', + ], + [ + $testScope, + 'inTryTwo', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'ternaryMatches', + TrinaryLogic::createYes(), + 'array{string}', + ], + [ + $testScope, + 'previousI', + TrinaryLogic::createYes(), + 'int<1, max>', + ], + [ + $testScope, + 'previousJ', + TrinaryLogic::createYes(), + '0', + ], + [ + $testScope, + 'frame', + TrinaryLogic::createYes(), + 'mixed~null', + ], + [ + $testScope, + 'listOne', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'listTwo', + TrinaryLogic::createYes(), + '2', + ], + [ + $testScope, + 'e', + TrinaryLogic::createYes(), + 'Exception', + ], + [ + $testScope, + 'exception', + TrinaryLogic::createYes(), + 'Exception', + ], + [ + $testScope, + 'inTryNotInCatch', + TrinaryLogic::createMaybe(), + '1', + ], + [ + $testScope, + 'fooObjectFromTryCatch', + TrinaryLogic::createYes(), + 'InTryCatchFoo', + ], + [ + $testScope, + 'mixedVarFromTryCatch', + TrinaryLogic::createYes(), + '1|1.0', + ], + [ + $testScope, + 'nullableIntegerFromTryCatch', + TrinaryLogic::createYes(), + '1|null', + ], + [ + $testScope, + 'anotherNullableIntegerFromTryCatch', + TrinaryLogic::createYes(), + '1|null', + ], + [ + $testScope, + 'nullableIntegers', + TrinaryLogic::createYes(), + 'array{1, 2, 3, null}', + ], + [ + $testScope, + 'union', + TrinaryLogic::createYes(), + 'array{1, 2, 3, \'foo\'}', + '1|2|3|\'foo\'', + ], + [ + $testScope, + 'trueOrFalse', + TrinaryLogic::createYes(), + 'bool', + ], + [ + $testScope, + 'falseOrTrue', + TrinaryLogic::createYes(), + 'bool', + ], + [ + $testScope, + 'true', + TrinaryLogic::createYes(), + 'true', + ], + [ + $testScope, + 'false', + TrinaryLogic::createYes(), + 'false', + ], + [ + $testScope, + 'trueOrFalseFromSwitch', + TrinaryLogic::createYes(), + 'bool', + ], + [ + $testScope, + 'trueOrFalseInSwitchWithDefault', + TrinaryLogic::createYes(), + 'bool', + ], + [ + $testScope, + 'trueOrFalseInSwitchInAllCases', + TrinaryLogic::createYes(), + 'bool', + ], + [ + $testScope, + 'trueOrFalseInSwitchInAllCasesWithDefault', + TrinaryLogic::createYes(), + 'bool', + ], + [ + $testScope, + 'trueOrFalseInSwitchInAllCasesWithDefaultCase', + TrinaryLogic::createYes(), + 'true', + ], + [ + $testScope, + 'variableDefinedInSwitchWithOtherCasesWithEarlyTermination', + TrinaryLogic::createYes(), + 'true', + ], + [ + $testScope, + 'anotherVariableDefinedInSwitchWithOtherCasesWithEarlyTermination', + TrinaryLogic::createYes(), + 'true', + ], + [ + $testScope, + 'variableDefinedOnlyInEarlyTerminatingSwitchCases', + TrinaryLogic::createNo(), + ], + [ + $testScope, + 'nullableTrueOrFalse', + TrinaryLogic::createYes(), + 'bool|null', + ], + [ + $testScope, + 'nonexistentVariableOutsideFor', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'integerOrNullFromFor', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'nonexistentVariableOutsideWhile', + TrinaryLogic::createMaybe(), + '1', + ], + [ + $testScope, + 'integerOrNullFromWhile', + TrinaryLogic::createYes(), + '1|null', + ], + [ + $testScope, + 'nonexistentVariableOutsideForeach', + TrinaryLogic::createMaybe(), + 'null', + ], + [ + $testScope, + 'integerOrNullFromForeach', + TrinaryLogic::createYes(), + '1|null', + ], + [ + $testScope, + 'notNullableString', + TrinaryLogic::createYes(), + 'string', + ], + [ + $testScope, + 'anotherNotNullableString', + TrinaryLogic::createYes(), + 'string', + ], + [ + $testScope, + 'notNullableObject', + TrinaryLogic::createYes(), + 'Foo', + ], + [ + $testScope, + 'nullableString', + TrinaryLogic::createYes(), + 'string|null', + ], + [ + $testScope, + 'alsoNotNullableString', + TrinaryLogic::createYes(), + 'string', + ], + [ + $testScope, + 'integerOrString', + TrinaryLogic::createYes(), + '\'str\'|int', + ], + [ + $testScope, + 'nullableIntegerAfterNeverCondition', + TrinaryLogic::createYes(), + 'int|null', + ], + [ + $testScope, + 'stillNullableInteger', + TrinaryLogic::createYes(), + '2|null', + ], + [ + $testScope, + 'arrayOfIntegers', + TrinaryLogic::createYes(), + 'array{1, 2, 3}', + ], + [ + $testScope, + 'arrayAccessObject', + TrinaryLogic::createYes(), + \ObjectWithArrayAccess\Foo::class, + ], + [ + $testScope, + 'width', + TrinaryLogic::createYes(), + '2.0', + ], + [ + $testScope, + 'someVariableThatWillGetOverrideInFinally', + TrinaryLogic::createYes(), + '\'foo\'', + ], + [ + $testScope, + 'maybeDefinedButLaterCertainlyDefined', + TrinaryLogic::createYes(), + '2|3', + ], + [ + $testScope, + 'mixed', + TrinaryLogic::createYes(), + 'mixed~bool', + ], + [ + $testScope, + 'variableDefinedInSwitchWithoutEarlyTermination', + TrinaryLogic::createMaybe(), + 'false', + ], + [ + $testScope, + 'anotherVariableDefinedInSwitchWithoutEarlyTermination', + TrinaryLogic::createMaybe(), + 'bool', + ], + [ + $testScope, + 'alwaysDefinedFromSwitch', + TrinaryLogic::createYes(), + '1|null', + ], + [ + $testScope, + 'exceptionFromTryCatch', + TrinaryLogic::createYes(), + '(AnotherException&Throwable)|(Throwable&YetAnotherException)|null', + ], + [ + $testScope, + 'nullOverwrittenInSwitchToOne', + TrinaryLogic::createYes(), + '1', + ], + [ + $testScope, + 'variableFromSwitchShouldBeBool', + TrinaryLogic::createYes(), + 'bool', + ], + ]; + } + + /** + * @dataProvider dataAssignInIf + */ + public function testAssignInIf( + Scope $scope, + string $variableName, + TrinaryLogic $expectedCertainty, + ?string $typeDescription = null, + ?string $iterableValueTypeDescription = null, + ): void + { + $this->assertVariables( + $scope, + $variableName, + $expectedCertainty, + $typeDescription, + $iterableValueTypeDescription, + ); + } + + public function dataConstantTypes(): array + { + $testScope = $this->getFileScope(__DIR__ . '/data/constantTypes.php'); + + return [ + [ + $testScope, + 'postIncrement', + '2', + ], + [ + $testScope, + 'postDecrement', + '4', + ], + [ + $testScope, + 'preIncrement', + '2', + ], + [ + $testScope, + 'preDecrement', + '4', + ], + [ + $testScope, + 'literalArray', + 'array{a: 2, b: 4, c: 2, d: 4}', + ], + [ + $testScope, + 'nullIncremented', + '1', + ], + [ + $testScope, + 'nullDecremented', + 'null', + ], + [ + $testScope, + 'incrementInIf', + '1|2|3', + ], + [ + $testScope, + 'anotherIncrementInIf', + '2|3', + ], + [ + $testScope, + 'valueOverwrittenInIf', + '1|2', + ], + [ + $testScope, + 'incrementInForLoop', + 'int<2, max>', + ], + [ + $testScope, + 'valueOverwrittenInForLoop', + '2', + ], + [ + $testScope, + 'arrayOverwrittenInForLoop', + 'array{a: int<2, max>, b: \'bar\'}', + ], + [ + $testScope, + 'anotherValueOverwrittenInIf', + '5|10', + ], + [ + $testScope, + 'intProperty', + 'int<2, max>', + ], + [ + $testScope, + 'staticIntProperty', + 'int<2, max>', + ], + [ + $testScope, + 'anotherIntProperty', + '1|2', + ], + [ + $testScope, + 'anotherStaticIntProperty', + '1|2', + ], + [ + $testScope, + 'variableIncrementedInClosurePassedByReference', + 'int<0, max>', + ], + [ + $testScope, + 'anotherVariableIncrementedInClosure', + '0', + ], + [ + $testScope, + 'yetAnotherVariableInClosurePassedByReference', + '0|1', + ], + [ + $testScope, + 'variableIncrementedInFinally', + '1', + ], + ]; + } + + /** + * @dataProvider dataConstantTypes + */ + public function testConstantTypes( + Scope $scope, + string $variableName, + string $typeDescription, + ): void + { + $this->assertVariables( + $scope, + $variableName, + TrinaryLogic::createYes(), + $typeDescription, + null, + ); + } + + private function assertVariables( + Scope $scope, + string $variableName, + TrinaryLogic $expectedCertainty, + ?string $typeDescription = null, + ?string $iterableValueTypeDescription = null, + ): void + { + $certainty = $scope->hasVariableType($variableName); + $this->assertTrue( + $expectedCertainty->equals($certainty), + sprintf( + 'Certainty of %s is %s, expected %s', + $variableName, + $certainty->describe(), + $expectedCertainty->describe(), + ), + ); + if (!$expectedCertainty->no()) { + if ($typeDescription === null) { + $this->fail(sprintf('Missing expected type for defined variable $%s.', $variableName)); + } + + $this->assertSame( + $typeDescription, + $scope->getVariableType($variableName)->describe(VerbosityLevel::precise()), + sprintf('Type of variable $%s does not match the expected one.', $variableName), + ); + + if ($iterableValueTypeDescription !== null) { + $this->assertSame( + $iterableValueTypeDescription, + $scope->getVariableType($variableName)->getIterableValueType()->describe(VerbosityLevel::precise()), + sprintf('Iterable value type of variable $%s does not match the expected one.', $variableName), + ); + } + } elseif ($typeDescription !== null) { + $this->fail( + sprintf( + 'No type should be asserted for an undefined variable $%s, %s given.', + $variableName, + $typeDescription, + ), + ); + } + } + + public function dataArrayDestructuring(): array + { + return [ + [ + 'mixed', + '$a', + ], + [ + 'mixed', + '$b', + ], + [ + 'mixed', + '$c', + ], + [ + 'mixed', + '$aList', + ], + [ + 'mixed', + '$bList', + ], + [ + 'mixed', + '$cList', + ], + [ + '1', + '$int', + ], + [ + '\'foo\'', + '$string', + ], + [ + 'true', + '$bool', + ], + [ + '*ERROR*', + '$never', + ], + [ + '*ERROR*', + '$nestedNever', + ], + [ + '1', + '$intList', + ], + [ + '\'foo\'', + '$stringList', + ], + [ + 'true', + '$boolList', + ], + [ + '*ERROR*', + '$neverList', + ], + [ + '*ERROR*', + '$nestedNeverList', + ], + [ + '1', + '$foreachInt', + ], + [ + 'false', + '$foreachBool', + ], + [ + '*ERROR*', + '$foreachNever', + ], + [ + '*ERROR*', + '$foreachNestedNever', + ], + [ + '1', + '$foreachIntList', + ], + [ + 'false', + '$foreachBoolList', + ], + [ + '*ERROR*', + '$foreachNeverList', + ], + [ + '*ERROR*', + '$foreachNestedNeverList', + ], + [ + '1|4', + '$u1', + ], + [ + '2|\'bar\'', + '$u2', + ], + [ + '3', + '$u3', + ], + [ + '1|4', + '$foreachU1', + ], + [ + '2|\'bar\'', + '$foreachU2', + ], + [ + '3', + '$foreachU3', + ], + [ + 'string', + '$firstStringArray', + ], + [ + 'string', + '$secondStringArray', + ], + [ + 'non-empty-string', + '$thirdStringArray', + ], + [ + 'string', + '$fourthStringArray', + ], + [ + 'string', + '$firstStringArrayList', + ], + [ + 'string', + '$secondStringArrayList', + ], + [ + 'non-empty-string', + '$thirdStringArrayList', + ], + [ + 'string', + '$fourthStringArrayList', + ], + [ + 'non-empty-string', + '$firstStringArrayForeach', + ], + [ + 'non-empty-string', + '$secondStringArrayForeach', + ], + [ + 'non-empty-string', + '$thirdStringArrayForeach', + ], + [ + 'non-empty-string', + '$fourthStringArrayForeach', + ], + [ + 'non-empty-string', + '$firstStringArrayForeachList', + ], + [ + 'non-empty-string', + '$secondStringArrayForeachList', + ], + [ + 'non-empty-string', + '$thirdStringArrayForeachList', + ], + [ + 'non-empty-string', + '$fourthStringArrayForeachList', + ], + [ + 'lowercase-string&uppercase-string', + '$dateArray[\'Y\']', + ], + [ + 'lowercase-string&uppercase-string', + '$dateArray[\'m\']', + ], + [ + 'int', + '$dateArray[\'d\']', + ], + [ + 'lowercase-string&uppercase-string', + '$intArrayForRewritingFirstElement[0]', + ], + [ + 'int', + '$intArrayForRewritingFirstElement[1]', + ], + [ + 'ArrayAccess&stdClass', + '$obj', + ], + [ + 'stdClass', + '$newArray[\'newKey\']', + ], + [ + 'true', + '$assocKey', + ], + [ + '\'foo\'', + '$assocFoo', + ], + [ + '1', + '$assocOne', + ], + [ + '*ERROR*', + '$assocNonExistent', + ], + [ + 'true', + '$dynamicAssocKey', + ], + [ + '\'123\'|true', + '$dynamicAssocStrings', + ], + [ + '1|\'123\'|\'foo\'|true', + '$dynamicAssocMixed', + ], + [ + 'true', + '$dynamicAssocKeyForeach', + ], + [ + '\'123\'|true', + '$dynamicAssocStringsForeach', + ], + [ + '1|\'123\'|\'foo\'|true', + '$dynamicAssocMixedForeach', + ], + [ + 'string', + '$stringFromIterable', + ], + [ + 'string', + '$stringWithVarAnnotation', + ], + [ + 'string', + '$stringWithVarAnnotationInForeach', + ], + ]; + } + + /** + * @dataProvider dataArrayDestructuring + */ + public function testArrayDestructuring( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-destructuring.php', + $description, + $expression, + ); + } + + public function dataParameterTypes(): array + { + return [ + [ + 'int', + '$integer', + ], + [ + 'bool', + '$boolean', + ], + [ + 'string', + '$string', + ], + [ + 'float', + '$float', + ], + [ + 'TypesNamespaceTypehints\Lorem', + '$loremObject', + ], + [ + 'mixed', + '$mixed', + ], + [ + 'array', + '$array', + ], + [ + 'bool|null', + '$isNullable', + ], + [ + 'TypesNamespaceTypehints\Lorem', + '$loremObjectRef', + ], + [ + 'TypesNamespaceTypehints\Bar', + '$barObject', + ], + [ + 'TypesNamespaceTypehints\Foo', + '$fooObject', + ], + [ + 'TypesNamespaceTypehints\Bar', + '$anotherBarObject', + ], + [ + 'callable(): mixed', + '$callable', + ], + [ + PHP_VERSION_ID < 80000 ? 'list' : 'array', + '$variadicStrings', + ], + [ + 'string', + '$variadicStrings[0]', + ], + ]; + } + + /** + * @dataProvider dataParameterTypes + */ + public function testTypehints( + string $typeClass, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/typehints.php', + $typeClass, + $expression, + ); + } + + public function dataAnonymousFunctionParameterTypes(): array + { + return [ + [ + 'int', + '$integer', + ], + [ + 'bool', + '$boolean', + ], + [ + 'string', + '$string', + ], + [ + 'float', + '$float', + ], + [ + 'TypesNamespaceTypehints\Lorem', + '$loremObject', + ], + [ + 'mixed', + '$mixed', + ], + [ + 'array', + '$array', + ], + [ + 'bool|null', + '$isNullable', + ], + [ + 'callable(): mixed', + '$callable', + ], + [ + 'TypesNamespaceTypehints\FooWithAnonymousFunction', + '$self', + ], + ]; + } + + /** + * @dataProvider dataAnonymousFunctionParameterTypes + */ + public function testAnonymousFunctionTypehints( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/typehints-anonymous-function.php', + $description, + $expression, + ); + } + + public function dataVarAnnotations(): array + { + return [ + [ + 'int', + '$integer', + ], + [ + 'bool', + '$boolean', + ], + [ + 'string', + '$string', + ], + [ + 'float', + '$float', + ], + [ + 'VarAnnotations\Lorem', + '$loremObject', + ], + [ + 'AnotherNamespace\Bar', + '$barObject', + ], + [ + 'mixed', + '$mixed', + ], + [ + 'array', + '$array', + ], + [ + 'bool|null', + '$isNullable', + ], + [ + 'callable(): mixed', + '$callable', + ], + [ + 'callable(int, string ...): void', + '$callableWithTypes', + ], + [ + 'Closure(int, string ...): void', + '$closureWithTypes', + ], + [ + 'VarAnnotations\Foo', + '$self', + ], + [ + 'float', + '$invalidInteger', + ], + [ + 'static(VarAnnotations\Foo)', + '$static', + ], + ]; + } + + /** + * @dataProvider dataVarAnnotations + */ + public function testVarAnnotations( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/var-annotations.php', + $description, + $expression, + 'die', + [], + false, + ); + } + + public function dataCasts(): array + { + return [ + [ + 'int', + '$castedInteger', + ], + [ + 'bool', + '$castedBoolean', + ], + [ + 'float', + '$castedFloat', + ], + [ + 'string', + '$castedString', + ], + [ + 'array', + '$castedArray', + ], + [ + 'stdClass', + '$castedObject', + ], + [ + 'TypesNamespaceCasts\Foo', + '$castedFoo', + ], + [ + 'stdClass|TypesNamespaceCasts\Foo', + '$castedArrayOrObject', + ], + [ + '0|1', + '(int) $bool', + ], + [ + '0.0|1.0', + '(float) $bool', + ], + [ + '*ERROR*', + '(int) $foo', + ], + [ + 'true', + '(bool) $foo', + ], + [ + '1', + '(int) true', + ], + [ + '0', + '(int) false', + ], + [ + '5', + '(int) 5.25', + ], + [ + '5.0', + '(float) 5', + ], + [ + '5', + '(int) "5"', + ], + [ + '5.0', + '(float) "5"', + ], + [ + '0', + '(int) "blabla"', + ], + [ + '0.0', + '(float) "blabla"', + ], + [ + '0', + '(int) null', + ], + [ + '0.0', + '(float) null', + ], + [ + 'int', + '(int) $str', + ], + [ + 'float', + '(float) $str', + ], + [ + "array{\0TypesNamespaceCasts\\Foo\0foo: TypesNamespaceCasts\\Foo, \0TypesNamespaceCasts\\Foo\0int: int, \0*\0protectedInt: int, publicInt: int, \0TypesNamespaceCasts\\Bar\0barProperty: TypesNamespaceCasts\\Bar}", + '(array) $foo', + ], + [ + 'array{1, 2, 3}', + '(array) [1, 2, 3]', + ], + [ + 'array{1}', + '(array) 1', + ], + [ + 'array{1.0}', + '(array) 1.0', + ], + [ + 'array{true}', + '(array) true', + ], + [ + 'array{\'blabla\'}', + '(array) "blabla"', + ], + [ + 'array{int}', + '(array) $castedInteger', + ], + [ + 'array', + '(array) $iterable', + ], + [ + 'array', + '(array) new stdClass()', + ], + ]; + } + + /** + * @dataProvider dataCasts + */ + public function testCasts( + string $desciptiion, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/casts.php', + $desciptiion, + $expression, + ); + } + + public function dataDeductedTypes(): array + { + return [ + [ + '1', + '$integerLiteral', + ], + [ + 'true', + '$booleanLiteral', + ], + [ + 'false', + '$anotherBooleanLiteral', + ], + [ + '\'foo\'', + '$stringLiteral', + ], + [ + '1.0', + '$floatLiteral', + ], + [ + '1.0', + '$floatAssignedByRef', + ], + [ + 'null', + '$nullLiteral', + ], + [ + 'TypesNamespaceDeductedTypes\Lorem', + '$loremObjectLiteral', + ], + [ + 'object', + '$mixedObjectLiteral', + ], + [ + 'static(TypesNamespaceDeductedTypes\Foo)', + '$newStatic', + ], + [ + 'array{}', + '$arrayLiteral', + ], + [ + 'string', + '$stringFromFunction', + ], + [ + 'TypesNamespaceFunctions\Foo', + '$fooObjectFromFunction', + ], + [ + 'mixed', + '$mixedFromFunction', + ], + [ + '1', + '\TypesNamespaceDeductedTypes\Foo::INTEGER_CONSTANT', + ], + [ + '1', + 'self::INTEGER_CONSTANT', + ], + [ + '1.0', + 'self::FLOAT_CONSTANT', + ], + [ + '\'foo\'', + 'self::STRING_CONSTANT', + ], + [ + 'array{}', + 'self::ARRAY_CONSTANT', + ], + [ + 'true', + 'self::BOOLEAN_CONSTANT', + ], + [ + 'null', + 'self::NULL_CONSTANT', + ], + [ + '1', + '$foo::INTEGER_CONSTANT', + ], + [ + '1.0', + '$foo::FLOAT_CONSTANT', + ], + [ + '\'foo\'', + '$foo::STRING_CONSTANT', + ], + [ + 'array{}', + '$foo::ARRAY_CONSTANT', + ], + [ + 'true', + '$foo::BOOLEAN_CONSTANT', + ], + [ + 'null', + '$foo::NULL_CONSTANT', + ], + ]; + } + + /** + * @dataProvider dataDeductedTypes + */ + public function testDeductedTypes( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/function-definitions.php'; + $this->assertTypes( + __DIR__ . '/data/deducted-types.php', + $description, + $expression, + ); + } + + public function dataProperties(): array + { + return [ + [ + 'mixed', + '$this->mixedProperty', + ], + [ + 'mixed', + '$this->anotherMixedProperty', + ], + [ + 'mixed', + '$this->yetAnotherMixedProperty', + ], + [ + 'int', + '$this->integerProperty', + ], + [ + 'int', + '$this->anotherIntegerProperty', + ], + [ + 'array', + '$this->arrayPropertyOne', + ], + [ + 'array', + '$this->arrayPropertyOther', + ], + [ + 'PropertiesNamespace\\Lorem', + '$this->objectRelative', + ], + [ + 'SomeOtherNamespace\\Ipsum', + '$this->objectFullyQualified', + ], + [ + 'SomeNamespace\\Amet', + '$this->objectUsed', + ], + [ + '*ERROR*', + '$this->nonexistentProperty', + ], + [ + 'int|null', + '$this->nullableInteger', + ], + [ + 'SomeNamespace\Amet|null', + '$this->nullableObject', + ], + [ + 'PropertiesNamespace\\Foo', + '$this->selfType', + ], + [ + 'static(PropertiesNamespace\Foo)', + '$this->staticType', + ], + [ + 'null', + '$this->nullType', + ], + [ + 'SomeNamespace\Sit', + '$this->inheritedProperty', + ], + [ + 'PropertiesNamespace\Bar', + '$this->barObject->doBar()', + ], + [ + 'mixed', + '$this->invalidTypeProperty', + ], + [ + 'resource', + '$this->resource', + ], + [ + 'mixed', + '$this->yetAnotherAnotherMixedParameter', + ], + [ + 'mixed', + '$this->yetAnotherAnotherAnotherMixedParameter', + ], + [ + 'string', + 'self::$staticStringProperty', + ], + [ + 'SomeGroupNamespace\One', + '$this->groupUseProperty', + ], + [ + 'SomeGroupNamespace\Two', + '$this->anotherGroupUseProperty', + ], + [ + 'PropertiesNamespace\Bar', + '$this->inheritDocProperty', + ], + [ + 'PropertiesNamespace\Bar', + '$this->inheritDocWithoutCurlyBracesProperty', + ], + [ + 'PropertiesNamespace\Bar', + '$this->implicitInheritDocProperty', + ], + [ + 'int', + '$this->readOnlyProperty', + ], + [ + 'string', + '$this->overriddenReadOnlyProperty', + ], + [ + 'string', + '$this->documentElement', + ], + ]; + } + + /** + * @dataProvider dataProperties + */ + public function testProperties( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/properties.php', + $description, + $expression, + ); + } + + public function dataBinaryOperations(): array + { + $typeCallback = static function ($value): string { + if (is_int($value)) { + return (new ConstantIntegerType($value))->describe(VerbosityLevel::precise()); + } elseif (is_float($value)) { + return (new ConstantFloatType($value))->describe(VerbosityLevel::precise()); + } elseif (is_bool($value)) { + return (new ConstantBooleanType($value))->describe(VerbosityLevel::precise()); + } elseif (is_string($value)) { + return (new ConstantStringType($value))->describe(VerbosityLevel::precise()); + } + + throw new ShouldNotHappenException(); + }; + + return [ + [ + 'false', + 'true && false', + ], + [ + 'true', + 'true || false', + ], + [ + 'true', + 'true xor false', + ], + [ + 'true', + 'false xor true', + ], + [ + 'false', + 'true xor true', + ], + [ + 'false', + 'true xor true', + ], + [ + 'bool', + '$bool xor true', + ], + [ + 'bool', + '$bool xor false', + ], + [ + 'false', + 'true and false', + ], + [ + 'true', + 'true or false', + ], + [ + 'false', + '!true', + ], + [ + $typeCallback(-1), + '-1', + ], + [ + $typeCallback(+1), + '+1', + ], + [ + '*ERROR*', + '+"blabla"', + ], + [ + '123.2', + '+"123.2"', + ], + [ + '*ERROR*', + '-"blabla"', + ], + [ + '-5', + '-5', + ], + [ + '5', + '-(-5)', + ], + [ + 'int', + '-$integer', + ], + [ + '-2|-1', + '-$conditionalInt', + ], + [ + '*ERROR*', + '-$string', + ], + // integer + integer + [ + $typeCallback(1 + 1), + '1 + 1', + ], + [ + $typeCallback(1 - 1), + '1 - 1', + ], + [ + $typeCallback(1 / 2), + '1 / 2', + ], + [ + $typeCallback(1 * 1), + '1 * 1', + ], + [ + $typeCallback(1 ** 1), + '1 ** 1', + ], + [ + $typeCallback(1 % 1), + '1 % 1', + ], + [ + '(float|int)', + '$integer /= 2', + ], + [ + 'int', + '$integer *= 1', + ], + // float + float + [ + $typeCallback(1.2 + 1.4), + '1.2 + 1.4', + ], + [ + $typeCallback(1.2 - 1.4), + '1.2 - 1.4', + ], + [ + $typeCallback(1.2 / 2.4), + '1.2 / 2.4', + ], + [ + $typeCallback(1.2 * 1.4), + '1.2 * 1.4', + ], + [ + $typeCallback(1.2 ** 1.4), + '1.2 ** 1.4', + ], + [ + '1', + '3.2 % 2.4', + ], + [ + 'float', + '$float /= 2.4', + ], + [ + 'float', + '$float *= 2.4', + ], + // integer + float + [ + $typeCallback(1 + 1.4), + '1 + 1.4', + ], + [ + $typeCallback(1 - 1.4), + '1 - 1.4', + ], + [ + $typeCallback(1 / 2.4), + '1 / 2.4', + ], + [ + $typeCallback(1 * 1.4), + '1 * 1.4', + ], + [ + $typeCallback(1 ** 1.4), + '1 ** 1.4', + ], + [ + '1', + '3 % 2.4', + ], + [ + 'float', + '$integer /= 2.4', + ], + [ + 'float', + '$integer *= 2.4', + ], + [ + 'int', + '$otherInteger + 1', + ], + [ + 'float', + '$otherInteger + 1.0', + ], + // float + integer + [ + $typeCallback(1.2 + 1), + '1.2 + 1', + ], + [ + $typeCallback(1.2 - 1), + '1.2 - 1', + ], + [ + $typeCallback(1.2 / 2), + '1.2 / 2', + ], + [ + $typeCallback(1.2 * 1), + '1.2 * 1', + ], + [ + 'int', + '$integer * 10', + ], + [ + $typeCallback(1.2 ** 1), + '1.2 ** 1', + ], + [ + '(float|int)', + '$integer ** $integer', + ], + [ + '1', + '3.2 % 2', + ], + [ + 'int', + '$float %= 2.4', + ], + [ + 'float', + '$float **= 2.4', + ], + [ + 'float', + '$float /= 2.4', + ], + [ + 'float', + '$float *= 2', + ], + // boolean + [ + '1', + 'true + false', + ], + // string + [ + "'ab'", + "'a' . 'b'", + ], + [ + $typeCallback(1 . 'b'), + "1 . 'b'", + ], + [ + $typeCallback(1.0 . 'b'), + "1.0 . 'b'", + ], + [ + $typeCallback(1.0 . 2.0), + '1.0 . 2.0', + ], + [ + $typeCallback('foo' <=> 'bar'), + "'foo' <=> 'bar'", + ], + [ + '(float|int)', + '1 + $mixed', + ], + [ + 'float|int', + '1 + $number', + ], + [ + 'float|int', + '$integer + $number', + ], + [ + 'float', + '$float + $float', + ], + [ + 'float', + '$float + $number', + ], + [ + '(float|int)', + '1 / $mixed', + ], + [ + 'float|int', + '1 / $number', + ], + [ + 'float', + '1.0 / $mixed', + ], + [ + 'float', + '1.0 / $number', + ], + [ + '(float|int)', + '$mixed / 1', + ], + [ + 'float|int', + '$number / 1', + ], + [ + 'float', + '$mixed / 1.0', + ], + [ + 'float', + '$number / 1.0', + ], + [ + 'float', + '1.0 + $mixed', + ], + [ + 'float', + '1.0 + $number', + ], + [ + '(float|int)', + '$mixed + 1', + ], + [ + 'float|int', + '$number + 1', + ], + [ + 'float', + '$mixed + 1.0', + ], + [ + 'float', + '$number + 1.0', + ], + [ + '\'foo\'|null', + '$mixed ? "foo" : null', + ], + [ + '12', + '12 ?: null', + ], + [ + '1', + 'true ? 1 : 2', + ], + [ + '2', + 'false ? 1 : 2', + ], + [ + '12|non-falsy-string', + '$string ?: 12', + ], + [ + '12|non-falsy-string', + '$stringOrNull ?: 12', + ], + [ + '12|non-falsy-string', + '@$stringOrNull ?: 12', + ], + [ + 'int|int<1, max>', + '$integer ?: 12', + ], + [ + '\'foo\'', + "'foo' ?? null", // "else" never gets executed + ], + [ + 'string|null', + '$stringOrNull ?? null', + ], + [ + '\'bar\'|\'foo\'', + '$maybeDefinedVariable ?? \'bar\'', + ], + [ + 'string', + '$string ?? \'foo\'', + ], + [ + 'string', + '$stringOrNull ?? \'foo\'', + ], + [ + 'string', + '$string ?? $integer', + ], + [ + 'int|string', + '$stringOrNull ?? $integer', + ], + [ + '\'Foo\'', + '\Foo::class', + ], + [ + '74', + '$line', + ], + [ + 'literal-string&non-falsy-string', + '$dir', + ], + [ + 'literal-string&non-falsy-string', + '$file', + ], + [ + '\'BinaryOperations\\\\NestedNamespace\'', + '$namespace', + ], + [ + '\'BinaryOperations\\\\NestedNamespace\\\\Foo\'', + '$class', + ], + [ + '\'BinaryOperations\\\\NestedNamespace\\\\Foo::doFoo\'', + '$method', + ], + [ + '\'doFoo\'', + '$function', + ], + [ + '1', + 'min([1, 2, 3])', + ], + [ + 'array{1, 2, 3}', + 'min([1, 2, 3], [4, 5, 5])', + ], + [ + '1', + 'min(...[1, 2, 3])', + ], + [ + '1', + 'min(...[2, 3, 4], ...[5, 1, 8])', + ], + [ + '0', + 'min(0, ...[1, 2, 3])', + ], + [ + 'array{5, 6, 9}', + 'max([1, 10, 8], [5, 6, 9])', + ], + [ + 'array{1, 1, 1, 1}', + 'max(array(2, 2, 2), array(1, 1, 1, 1))', + ], + [ + 'array', + 'max($arrayOfUnknownIntegers, $arrayOfUnknownIntegers)', + ], + [ + '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])', + ], + [ + '1.1', + 'min(...[1.1, 2, 3])', + ], + [ + '3', + 'max(...[1, 2, 3])', + ], + [ + '3.3', + 'max(...[1.1, 2.2, 3.3])', + ], + [ + '1', + 'min(1, 2, 3)', + ], + [ + '3', + 'max(1, 2, 3)', + ], + [ + '1.1', + 'min(1.1, 2.2, 3.3)', + ], + [ + '3.3', + 'max(1.1, 2.2, 3.3)', + ], + [ + '1', + 'min(1, 1)', + ], + [ + '*ERROR*', + 'min(1)', + ], + [ + 'int|string', + 'min($integer, $string)', + ], + [ + 'int|string', + 'min([$integer, $string])', + ], + [ + 'int|string', + 'min(...[$integer, $string])', + ], + [ + '\'a\'', + 'min(\'a\', \'b\')', + ], + [ + 'DateTimeImmutable', + 'max(new \DateTimeImmutable("today"), new \DateTimeImmutable("tomorrow"))', + ], + [ + '1', + 'min(1, 2.2, 3.3)', + ], + [ + 'non-falsy-string', + '"Hello $world"', + ], + [ + 'non-falsy-string', + '$string .= "str"', + ], + [ + 'int', + '$integer <<= 2.2', + ], + [ + 'int', + '$float >>= 2.2', + ], + [ + '3', + 'count($arrayOfIntegers)', + ], + [ + 'int<0, max>', + 'count($arrayOfIntegers, \COUNT_RECURSIVE)', + ], + [ + '3', + 'count($arrayOfIntegers, 5)', + ], + [ + '6', + 'count($arrayOfIntegers) + count($arrayOfIntegers)', + ], + [ + 'bool', + '$string === "foo"', + ], + [ + 'true', + '$fooString === "foo"', + ], + [ + 'bool', + '$string !== "foo"', + ], + [ + 'false', + '$fooString !== "foo"', + ], + [ + 'bool', + '$string == "foo"', + ], + [ + 'bool', + '$string != "foo"', + ], + [ + 'true', + '$foo instanceof \BinaryOperations\NestedNamespace\Foo', + ], + [ + 'bool', + '$foo instanceof Bar', + ], + [ + 'true', + 'isset($foo)', + ], + [ + 'true', + 'isset($foo, $one)', + ], + [ + 'false', + 'isset($null)', + ], + [ + 'false', + 'isset($undefinedVariable)', + ], + [ + 'false', + 'isset($foo, $undefinedVariable)', + ], + [ + 'bool', + 'isset($stringOrNull)', + ], + [ + 'false', + 'isset($stringOrNull, $null)', + ], + [ + 'false', + 'isset($stringOrNull, $undefinedVariable)', + ], + [ + 'bool', + 'isset($foo, $stringOrNull)', + ], + [ + 'bool', + 'isset($foo, $stringOrNull)', + ], + [ + 'true', + 'isset($array[\'0\'])', + ], + [ + 'bool', + 'isset($array[$integer])', + ], + [ + 'false', + 'isset($array[$integer], $array[1000])', + ], + [ + 'false', + 'isset($array[$integer], $null)', + ], + [ + 'bool', + 'isset($array[\'0\'], $array[$integer])', + ], + [ + 'bool', + 'isset($foo, $array[$integer])', + ], + [ + 'false', + 'isset($foo, $array[1000])', + ], + [ + 'false', + 'isset($foo, $array[1000])', + ], + [ + 'false', + '!isset($foo)', + ], + [ + 'false', + 'empty($foo)', + ], + [ + 'true', + '!empty($foo)', + ], + [ + 'array{int, int, int}', + '$arrayOfIntegers + $arrayOfIntegers', + ], + [ + 'array{int, int, int}', + '$arrayOfIntegers += $arrayOfIntegers', + ], + [ + '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{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}', + '$unshiftedConditionalArray + $conditionalArray', + ], + [ + 'array{int, int, int}', + '$arrayOfIntegers += ["foo"]', + ], + [ + '*ERROR*', + '$arrayOfIntegers += "foo"', + ], + [ + '3', + '@count($arrayOfIntegers)', + ], + [ + 'array{int, int, int}', + '$anotherArray = $arrayOfIntegers', + ], + [ + '1', + '$one++', + ], + [ + '1', + '$one--', + ], + [ + '2', + '++$one', + ], + [ + '0', + '--$one', + ], + [ + '*ERROR*', + '$preIncArray[0]', + ], + [ + '1', + '$preIncArray[1]', + ], + [ + '2', + '$preIncArray[2]', + ], + [ + '*ERROR*', + '$preIncArray[3]', + ], + [ + 'array{1: 1, 2: 2}', + '$preIncArray', + ], + [ + 'array{0: 1, 2: 3}', + '$postIncArray', + ], + [ + 'array{0: array{1: array{2: 3}}, 4: array{5: array{6: 7}}}', + '$anotherPostIncArray', + ], + [ + '3', + 'count($array)', + ], + [ + 'int<0, max>', + 'count()', + ], + [ + 'int<0, max>', + 'count($appendingToArrayInBranches)', + ], + [ + '3|5', + 'count($conditionalArray)', + ], + [ + '2', + '$array[1]', + ], + [ + '(float|int)', + '$integer / $integer', + ], + [ + '(float|int)', + '$otherInteger / $integer', + ], + [ + '(array|float|int)', + '$mixed + $mixed', + ], + [ + '(float|int)', + '$mixed - $mixed', + ], + [ + '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"', + ], + [ + '124.2', + '1 + "123.2"', + ], + [ + '*ERROR*', + '1 + $string', + ], + [ + '*ERROR*', + '1 + "blabla"', + ], + [ + 'array{1, 2, 3}', + '[1, 2, 3] + [4, 5, 6]', + ], + [ + 'non-empty-array', + '$arrayOfUnknownIntegers + [1, 2, 3]', + ], + [ + '(float|int)', + '$sumWithStaticConst', + ], + [ + '(float|int)', + '$severalSumWithStaticConst1', + ], + [ + '(float|int)', + '$severalSumWithStaticConst2', + ], + [ + '(float|int)', + '$severalSumWithStaticConst3', + ], + [ + '1', + '5 & 3', + ], + [ + 'int<0, 3>', + '$integer & 3', + ], + [ + 'int<0, 7>', + '7 & $integer', + ], + [ + 'int', + '$integer & $integer', + ], + [ + '\'x\'', + '"x" & "y"', + ], + [ + 'string', + '$string & "x"', + ], + [ + '*ERROR*', + '"bla" & 3', + ], + [ + '1', + '"5" & 3', + ], + [ + '7', + '5 | 3', + ], + [ + 'int', + '$integer | 3', + ], + [ + '\'y\'', + '"x" | "y"', + ], + [ + 'string', + '$string | "x"', + ], + [ + '*ERROR*', + '"bla" | 3', + ], + [ + '7', + '"5" | 3', + ], + [ + '6', + '5 ^ 3', + ], + [ + 'int', + '$integer ^ 3', + ], + [ + '"\001"', + '"x" ^ "y"', + ], + [ + 'string', + '$string ^ "x"', + ], + [ + '*ERROR*', + '"bla" ^ 3', + ], + [ + '6', + '"5" ^ 3', + ], + [ + 'int<0, 3>', + '$integer &= 3', + ], + [ + '*ERROR*', + '$string &= 3', + ], + [ + 'string', + '$string &= "x"', + ], + [ + 'int', + '$integer |= 3', + ], + [ + '*ERROR*', + '$string |= 3', + ], + [ + 'string', + '$string |= "x"', + ], + [ + 'int', + '$integer ^= 3', + ], + [ + '*ERROR*', + '$string ^= 3', + ], + [ + 'string', + '$string ^= "x"', + ], + [ + '\'f\'', + '$fooString[0]', + ], + [ + '*ERROR*', + '$fooString[4]', + ], + [ + "''|'f'|'o'", + '$fooString[$integer]', + ], + [ + '\'foo bar\'', + '$foobarString', + ], + [ + '\'foo bar\'', + '"$fooString bar"', + ], + [ + 'non-falsy-string', + '"$std bar"', + ], + [ + 'non-empty-array<\'foo\'|int|stdClass>', + '$arrToPush', + ], + [ + 'non-empty-array<\'foo\'|int|stdClass>', + '$arrToPush2', + ], + [ + 'array{0: \'lorem\', 1: 5, foo: stdClass, 2: \'test\'}', + '$arrToUnshift', + ], + [ + 'non-empty-array<\'lorem\'|int|stdClass>', + '$arrToUnshift2', + ], + [ + 'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}', + '$unshiftedConditionalArray', + ], + [ + 'array{dirname?: string, basename: string, extension?: string, filename: string}', + 'pathinfo($string)', + ], + [ + 'string', + 'pathinfo($string, PATHINFO_DIRNAME)', + ], + [ + 'string', + '$string++', + ], + [ + 'string', + '$string--', + ], + [ + '(float|int|string)', + '++$string', + ], + [ + '(float|int|string)', + '--$string', + ], + [ + '(float|int|string)', + '$incrementedString', + ], + [ + '(float|int|string)', + '$decrementedString', + ], + [ + '\'foo\'', + '$fooString++', + ], + [ + '\'foo\'', + '$fooString--', + ], + [ + '\'fop\'', + '++$fooString', + ], + [ + '\'foo\'', + '--$fooString', + ], + [ + '\'fop\'', + '$incrementedFooString', + ], + [ + '\'foo\'', + '$decrementedFooString', + ], + [ + "'barbar'|'barfoo'|'foobar'|'foofoo'", + '$conditionalString . $conditionalString', + ], + [ + "'baripsum'|'barlorem'|'fooipsum'|'foolorem'", + '$conditionalString . $anotherConditionalString', + ], + [ + "'ipsumbar'|'ipsumfoo'|'lorembar'|'loremfoo'", + '$anotherConditionalString . $conditionalString', + ], + [ + '6|8', + 'count($conditionalArray) + count($array)', + ], + [ + 'bool', + 'is_numeric($string)', + ], + [ + 'false', + 'is_numeric($fooString)', + ], + [ + 'bool', + 'is_int($mixed)', + ], + [ + 'true', + 'is_int($integer)', + ], + [ + 'false', + 'is_int($string)', + ], + [ + 'bool', + 'in_array(\'foo\', [\'foo\', \'bar\'])', + ], + [ + 'true', + 'in_array(\'foo\', [\'foo\', \'bar\'], true)', + ], + [ + 'false', + 'in_array(\'baz\', [\'foo\', \'bar\'], true)', + ], + [ + 'array{2, 3}', + '$arrToShift', + ], + [ + 'array{1, 2}', + '$arrToPop', + ], + [ + 'class-string', + 'static::class', + ], + [ + '\'NonexistentClass\'', + 'NonexistentClass::class', + ], + [ + 'class-string', + 'parent::class', + ], + [ + 'true', + 'array_key_exists(0, $array)', + ], + [ + 'false', + 'array_key_exists(3, $array)', + ], + [ + 'bool', + 'array_key_exists(3, $conditionalArray)', + ], + [ + 'bool', + 'array_key_exists(\'foo\', $generalArray)', + ], + [ + 'string', + 'sprintf($string, $string, 1)', + ], + [ + '\'foo bar\'', + "sprintf('%s %s', 'foo', 'bar')", + ], + [ + 'array{}|array{\'password\'}|array{0: \'username\', 1?: \'password\'}', + '$coalesceArray', + ], + [ + 'array{1, 2, 3}', + '$arrayToBeUnset', + ], + [ + 'array{1, 2, 3}', + '$arrayToBeUnset2', + ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset3', + ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset4', + ], + [ + 'array', + '$shiftedNonEmptyArray', + ], + [ + 'non-empty-array', + '$unshiftedArray', + ], + [ + 'array', + '$poppedNonEmptyArray', + ], + [ + 'non-empty-array', + '$pushedArray', + ], + [ + 'string|false', + '$simpleXMLReturningXML', + ], + [ + 'non-falsy-string', + '$xmlString', + ], + [ + 'bool', + '$simpleXMLWritingXML', + ], + [ + 'array', + '$simpleXMLRightXpath', + ], + [ + 'array|false|null', + '$simpleXMLWrongXpath', + ], + [ + 'array|false|null', + '$simpleXMLUnknownXpath', + ], + [ + 'array|false|null', + '$namespacedXpath', + ], + ]; + } + + /** + * @dataProvider dataBinaryOperations + */ + public function testBinaryOperations( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/binary.php', + $description, + $expression, + ); + } + + public function dataVarStatementAnnotation(): array + { + return [ + [ + 'VarStatementAnnotation\Foo', + '$object', + ], + ]; + } + + /** + * @dataProvider dataVarStatementAnnotation + */ + public function testVarStatementAnnotation( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/var-stmt-annotation.php', + $description, + $expression, + ); + } + + public function dataCloneOperators(): array + { + return [ + [ + 'CloneOperators\Foo', + 'clone $fooObject', + ], + ]; + } + + /** + * @dataProvider dataCloneOperators + */ + public function testCloneOperators( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/clone.php', + $description, + $expression, + ); + } + + public function dataLiteralArrays(): array + { + return [ + [ + '0', + '$integers[0]', + ], + [ + '1', + '$integers[1]', + ], + [ + '\'foo\'', + '$strings[0]', + ], + [ + '*ERROR*', + '$emptyArray[0]', + ], + [ + '0', + '$mixedArray[0]', + ], + [ + 'true', + '$integers[0] >= $integers[1] - 1', + ], + [ + 'array{foo: array{foo: array{foo: \'bar\'}}, bar: array{}, baz: array{lorem: array{}}}', + '$nestedArray', + ], + [ + '0', + '$integers[\'0\']', + ], + ]; + } + + /** + * @dataProvider dataLiteralArrays + */ + public function testLiteralArrays( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/literal-arrays.php', + $description, + $expression, + ); + } + + public function dataLiteralArraysKeys(): array + { + define('STRING_ONE', '1'); + define('INT_ONE', 1); + define('STRING_FOO', 'foo'); + + return [ + [ + '0|1|2', + "'NoKeysArray'", + ], + [ + '0|1|2', + "'IntegersAndNoKeysArray'", + ], + [ + '0|1|\'foo\'', + "'StringsAndNoKeysArray'", + ], + [ + '1|2|3', + "'IntegersAsStringsAndNoKeysArray'", + ], + [ + '1|2', + "'IntegersAsStringsArray'", + ], + [ + '1|2', + "'IntegersArray'", + ], + [ + '1|2|3', + "'IntegersWithFloatsArray'", + ], + [ + '\'bar\'|\'foo\'', + "'StringsArray'", + ], + [ + '\'\'|\'bar\'|\'baz\'', + "'StringsWithNullArray'", + ], + [ + '1|2|string', + "'IntegersWithStringFromMethodArray'", + ], + [ + '1|2|\'foo\'', + "'IntegersAndStringsArray'", + ], + [ + '0|1', + "'BooleansArray'", + ], + [ + '(int|string)', + "'UnknownConstantArray'", + ], + ]; + } + + /** + * @dataProvider dataLiteralArraysKeys + */ + public function testLiteralArraysKeys( + string $description, + string $evaluatedPointExpressionType, + ): void + { + $this->assertTypes( + __DIR__ . '/data/literal-arrays-keys.php', + $description, + '$key', + $evaluatedPointExpressionType, + ); + } + + public function dataStringArrayAccess(): array + { + return [ + [ + '*ERROR*', + '$stringFalse', + ], + [ + '*ERROR*', + '$stringObject', + ], + [ + '*ERROR*', + '$stringFloat', + ], + [ + '*ERROR*', + '$stringString', + ], + [ + '*ERROR*', + '$stringArray', + ], + ]; + } + + /** + * @dataProvider dataStringArrayAccess + */ + public function testStringArrayAccess( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/string-array-access.php', + $description, + $expression, + ); + } + + public function dataTypeFromFunctionPhpDocs(): array + { + return [ + [ + 'mixed', + '$mixedParameter', + ], + [ + 'MethodPhpDocsNamespace\Bar|MethodPhpDocsNamespace\Foo', + '$unionTypeParameter', + ], + [ + 'int', + '$anotherMixedParameter', + ], + [ + 'mixed', + '$yetAnotherMixedParameter', + ], + [ + 'int', + '$integerParameter', + ], + [ + 'int', + '$anotherIntegerParameter', + ], + [ + 'array', + '$arrayParameterOne', + ], + [ + 'array', + '$arrayParameterOther', + ], + [ + 'MethodPhpDocsNamespace\\Lorem', + '$objectRelative', + ], + [ + 'SomeOtherNamespace\\Ipsum', + '$objectFullyQualified', + ], + [ + 'SomeNamespace\\Amet', + '$objectUsed', + ], + [ + '*ERROR*', + '$nonexistentParameter', + ], + [ + 'int|null', + '$nullableInteger', + ], + [ + 'SomeNamespace\Amet|null', + '$nullableObject', + ], + [ + 'SomeNamespace\Amet|null', + '$anotherNullableObject', + ], + [ + 'null', + '$nullType', + ], + [ + 'MethodPhpDocsNamespace\Bar', + '$barObject->doBar()', + ], + [ + 'MethodPhpDocsNamespace\Bar', + '$conflictedObject', + ], + [ + 'MethodPhpDocsNamespace\Baz', + '$moreSpecifiedObject', + ], + [ + 'MethodPhpDocsNamespace\Baz', + '$moreSpecifiedObject->doFluent()', + ], + [ + 'MethodPhpDocsNamespace\Baz|null', + '$moreSpecifiedObject->doFluentNullable()', + ], + [ + 'MethodPhpDocsNamespace\Baz', + '$moreSpecifiedObject->doFluentArray()[0]', + ], + [ + 'iterable&MethodPhpDocsNamespace\Collection', + '$moreSpecifiedObject->doFluentUnionIterable()', + ], + [ + 'MethodPhpDocsNamespace\Baz', + '$fluentUnionIterableBaz', + ], + [ + 'resource', + '$resource', + ], + [ + 'mixed', + '$yetAnotherAnotherMixedParameter', + ], + [ + 'mixed', + '$yetAnotherAnotherAnotherMixedParameter', + ], + [ + 'void', + '$voidParameter', + ], + [ + 'SomeNamespace\Consecteur', + '$useWithoutAlias', + ], + [ + 'true', + '$true', + ], + [ + 'false', + '$false', + ], + [ + 'true', + '$boolTrue', + ], + [ + 'false', + '$boolFalse', + ], + [ + 'bool', + '$trueBoolean', + ], + [ + 'bool', + '$parameterWithDefaultValueFalse', + ], + ]; + } + + public function dataTypeFromFunctionFunctionPhpDocs(): array + { + return [ + [ + 'MethodPhpDocsNamespace\Foo', + '$fooFunctionResult', + ], + [ + 'MethodPhpDocsNamespace\Bar', + '$barFunctionResult', + ], + ]; + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromFunctionFunctionPhpDocs + */ + public function testTypeFromFunctionPhpDocs( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/functionPhpDocs.php'; + $this->assertTypes( + __DIR__ . '/data/functionPhpDocs.php', + $description, + $expression, + ); + } + + public function dataTypeFromFunctionPrefixedPhpDocs(): array + { + return [ + [ + 'MethodPhpDocsNamespace\Foo', + '$fooFunctionResult', + ], + ]; + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromFunctionPrefixedPhpDocs + */ + public function testTypeFromFunctionPhpDocsPsalmPrefix( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/functionPhpDocs-psalmPrefix.php'; + $this->assertTypes( + __DIR__ . '/data/functionPhpDocs-psalmPrefix.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromFunctionPrefixedPhpDocs + */ + public function testTypeFromFunctionPhpDocsPhpstanPrefix( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php'; + $this->assertTypes( + __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php', + $description, + $expression, + ); + } + + /** + * @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 [ + [ + 'MethodPhpDocsNamespace\\Foo', + '$selfType', + ], + [ + 'static(MethodPhpDocsNamespace\Foo)', + '$staticType', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$this->doFoo()', + ], + [ + 'MethodPhpDocsNamespace\Bar', + 'static::doSomethingStatic()', + ], + [ + 'static(MethodPhpDocsNamespace\Foo)', + 'parent::doLorem()', + ], + [ + 'MethodPhpDocsNamespace\FooParent', + '$parent->doLorem()', + false, + ], + [ + 'static(MethodPhpDocsNamespace\Foo)', + '$this->doLorem()', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$differentInstance->doLorem()', + ], + [ + 'static(MethodPhpDocsNamespace\Foo)', + 'parent::doIpsum()', + ], + [ + 'MethodPhpDocsNamespace\FooParent', + '$parent->doIpsum()', + false, + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$differentInstance->doIpsum()', + ], + [ + 'static(MethodPhpDocsNamespace\Foo)', + '$this->doIpsum()', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$this->doBar()[0]', + ], + [ + 'MethodPhpDocsNamespace\Bar', + 'self::doSomethingStatic()', + ], + [ + 'MethodPhpDocsNamespace\Bar', + '\MethodPhpDocsNamespace\Foo::doSomethingStatic()', + ], + [ + '$this(MethodPhpDocsNamespace\Foo)', + 'parent::doThis()', + ], + [ + '$this(MethodPhpDocsNamespace\Foo)|null', + 'parent::doThisNullable()', + ], + [ + '$this(MethodPhpDocsNamespace\Foo)|MethodPhpDocsNamespace\Bar|null', + 'parent::doThisUnion()', + ], + [ + 'MethodPhpDocsNamespace\FooParent', + '$this->returnParent()', + false, + ], + [ + 'MethodPhpDocsNamespace\FooParent', + '$this->returnPhpDocParent()', + false, + ], + [ + 'array', + '$this->returnNulls()', + ], + [ + 'object', + '$objectWithoutNativeTypehint', + ], + [ + 'object', + '$objectWithNativeTypehint', + ], + [ + 'object', + '$this->returnObject()', + ], + [ + 'MethodPhpDocsNamespace\FooParent', + 'new parent()', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$inlineSelf', + ], + [ + 'MethodPhpDocsNamespace\Bar', + '$inlineBar', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$this->phpDocVoidMethod()', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$this->phpDocVoidMethodFromInterface()', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$this->phpDocVoidParentMethod()', + ], + [ + 'MethodPhpDocsNamespace\Foo', + '$this->phpDocWithoutCurlyBracesVoidParentMethod()', + ], + [ + 'array', + '$this->returnsStringArray()', + ], + [ + 'mixed', + '$this->privateMethodWithPhpDoc()', + ], + ]; + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocs( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsPsalmPrefix( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPsalmPrefix)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPsalmPrefix)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooPsalmPrefix'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-psalmPrefix.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + * @param bool $replaceClass = true + */ + public function testTypeFromMethodPhpDocsPhpstanPrefix( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhpstanPrefix)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhpstanPrefix)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooPhpstanPrefix'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-phpstanPrefix.php', + $description, + $expression, + ); + } + + /** + * @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 + */ + public function testTypeFromTraitPhpDocs( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithTrait)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooWithTrait)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooWithTrait'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-trait.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + if ($replaceClass) { + $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\FooInheritDocChildWithoutCurly'; + } + } + $this->assertTypes( + __DIR__ . '/data/method-phpDocs-inheritdoc-without-curly-braces.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromRecursiveTraitPhpDocs( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithRecursiveTrait)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooWithRecursiveTrait)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooWithRecursiveTrait'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-recursiveTrait.php', + $description, + $expression, + ); + } + + public function dataTypeFromTraitPhpDocsInSameFile(): array + { + return [ + [ + 'string', + '$this->getFoo()', + ], + ]; + } + + /** + * @dataProvider dataTypeFromTraitPhpDocsInSameFile + */ + public function testTypeFromTraitPhpDocsInSameFile( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-traitInSameFileAsClass.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsInheritDoc( + string $description, + string $expression, + bool $replaceClass = true, + ): 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('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); + if ($expression === '$inlineSelf') { + $description = 'MethodPhpDocsNamespace\FooInheritDocChild'; + } + } + $this->assertTypes( + __DIR__ . '/data/method-phpDocs-inheritdoc.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsImplicitInheritance( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + if ($replaceClass) { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhpDocsImplicitInheritanceChild)', $description); + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhpDocsImplicitInheritanceChild)', $description); + $description = str_replace('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); + if ($expression === '$inlineSelf') { + $description = 'MethodPhpDocsNamespace\FooPhpDocsImplicitInheritanceChild'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-implicitInheritance.php', + $description, + $expression, + ); + } + + public function testNotSwitchInstanceof(): void + { + $this->assertTypes( + __DIR__ . '/data/switch-instanceof-not.php', + '*NEVER*', + '$foo', + ); + } + + public function dataSwitchInstanceOf(): array + { + return [ + [ + '*ERROR*', + '$foo', + ], + [ + '*ERROR*', + '$bar', + ], + [ + 'SwitchInstanceOf\Baz', + '$baz', + ], + ]; + } + + /** + * @dataProvider dataSwitchInstanceOf + */ + public function testSwitchInstanceof( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/switch-instanceof.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataSwitchInstanceOf + */ + public function testSwitchInstanceofTruthy( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/switch-instanceof-truthy.php', + $description, + $expression, + ); + } + + public function dataSwitchGetClass(): array + { + return [ + [ + 'SwitchGetClass\Lorem', + '$lorem', + "'normalName'", + ], + [ + 'SwitchGetClass\Foo', + '$lorem', + "'selfReferentialName'", + ], + ]; + } + + /** + * @dataProvider dataSwitchGetClass + */ + public function testSwitchGetClass( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/switch-get-class.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataSwitchInstanceOfFallthrough(): array + { + return [ + [ + 'SwitchInstanceOfFallthrough\A|SwitchInstanceOfFallthrough\B', + '$object', + ], + ]; + } + + /** + * @dataProvider dataSwitchInstanceOfFallthrough + */ + public function testSwitchInstanceOfFallthrough( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/switch-instanceof-fallthrough.php', + $description, + $expression, + ); + } + + public function dataSwitchTypeElimination(): array + { + return [ + [ + 'string', + '$stringOrInt', + ], + ]; + } + + /** + * @dataProvider dataSwitchTypeElimination + */ + public function testSwitchTypeElimination( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/switch-type-elimination.php', + $description, + $expression, + ); + } + + public function dataOverwritingVariable(): array + { + return [ + [ + 'mixed', + '$var', + 'new \OverwritingVariable\Bar()', + ], + [ + 'OverwritingVariable\Bar', + '$var', + '$var->methodFoo()', + ], + [ + 'OverwritingVariable\Foo', + '$var', + 'die', + ], + ]; + } + + /** + * @dataProvider dataOverwritingVariable + */ + public function testOverwritingVariable( + string $description, + string $expression, + string $evaluatedPointExpressionType, + ): void + { + $this->assertTypes( + __DIR__ . '/data/overwritingVariable.php', + $description, + $expression, + $evaluatedPointExpressionType, + ); + } + + public function dataNegatedInstanceof(): array + { + return [ + [ + 'NegatedInstanceOf\Foo', + '$foo', + ], + [ + 'NegatedInstanceOf\Bar', + '$bar', + ], + [ + 'mixed', + '$lorem', + ], + [ + 'mixed~NegatedInstanceOf\Dolor', + '$dolor', + ], + [ + 'mixed~NegatedInstanceOf\Sit', + '$sit', + ], + [ + 'mixed', + '$mixedFoo', + ], + [ + 'mixed', + '$mixedBar', + ], + [ + 'NegatedInstanceOf\Foo', + '$self', + ], + [ + 'static(NegatedInstanceOf\Foo)', + '$static', + ], + [ + 'NegatedInstanceOf\Foo', + '$anotherFoo', + ], + [ + 'NegatedInstanceOf\Bar&NegatedInstanceOf\Foo', + '$fooAndBar', + ], + ]; + } + + /** + * @dataProvider dataNegatedInstanceof + */ + public function testNegatedInstanceof( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/negated-instanceof.php', + $description, + $expression, + ); + } + + public function dataAnonymousFunction(): array + { + return [ + [ + 'string', + '$str', + ], + [ + PHP_VERSION_ID < 80000 ? 'list' : 'array', + '$arr', + ], + [ + '1', + '$integer', + ], + [ + '*ERROR*', + '$bar', + ], + ]; + } + + /** + * @dataProvider dataAnonymousFunction + */ + public function testAnonymousFunction( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/anonymous-function.php', + $description, + $expression, + ); + } + + public function dataForeachArrayType(): array + { + return [ + [ + __DIR__ . '/data/foreach/array-object-type.php', + 'AnotherNamespace\Foo', + '$foo', + ], + [ + __DIR__ . '/data/foreach/array-object-type.php', + 'AnotherNamespace\Foo', + '$foos[0]', + ], + [ + __DIR__ . '/data/foreach/array-object-type.php', + '0', + 'self::ARRAY_CONSTANT[0]', + ], + [ + __DIR__ . '/data/foreach/array-object-type.php', + '\'foo\'', + 'self::MIXED_CONSTANT[1]', + ], + [ + __DIR__ . '/data/foreach/nested-object-type.php', + 'AnotherNamespace\Foo', + '$foo', + ], + [ + __DIR__ . '/data/foreach/nested-object-type.php', + 'AnotherNamespace\Foo', + '$foos[0]', + ], + [ + __DIR__ . '/data/foreach/nested-object-type.php', + 'AnotherNamespace\Foo', + '$fooses[0][0]', + ], + [ + __DIR__ . '/data/foreach/integer-type.php', + 'int', + '$integer', + ], + [ + __DIR__ . '/data/foreach/reusing-specified-variable.php', + '1|2|3', + '$business', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-variable-first.php', + 'mixed', + '$value', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-variable-second.php', + 'stdClass', + '$value', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-no-variable.php', + 'bool', + '$value', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-no-variable-2.php', + '*ERROR*', + '$value', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-wrong-variable.php', + 'mixed', + '$value', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-variable-with-reference.php', + 'string', + '$value', + ], + [ + __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', + 'non-empty-array', + '$list', + ], + [ + __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', + 'string', + '$key', + ], + [ + __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', + 'float|int|string', + '$value', + ], + [ + __DIR__ . '/data/foreach/foreach-with-complex-value-type.php', + 'float|ForeachWithComplexValueType\Foo', + '$value', + ], + [ + __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', + 'ForeachWithGenericsPhpDocIterable\Bar|ForeachWithGenericsPhpDocIterable\Foo', + '$key', + ], + [ + __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', + 'float|int|string', + '$value', + ], + [ + __DIR__ . '/data/foreach/foreach-iterable-with-complex-value-type.php', + 'float|ForeachIterableWithComplexValueType\Foo', + '$value', + ], + [ + __DIR__ . '/data/foreach/type-in-comment-key.php', + 'int', + '$key', + ], + ]; + } + + /** + * @dataProvider dataForeachArrayType + */ + public function testForeachArrayType( + string $file, + string $description, + string $expression, + ): void + { + $this->assertTypes( + $file, + $description, + $expression, + ); + } + + public function dataOverridingSpecifiedType(): array + { + return [ + [ + __DIR__ . '/data/catch-specified-variable.php', + 'TryCatchWithSpecifiedVariable\FooException', + '$foo', + ], + ]; + } + + /** + * @dataProvider dataOverridingSpecifiedType + */ + public function testOverridingSpecifiedType( + string $file, + string $description, + string $expression, + ): void + { + $this->assertTypes( + $file, + $description, + $expression, + ); + } + + public function dataForeachObjectType(): array + { + return [ + [ + __DIR__ . '/data/foreach/object-type.php', + 'ObjectType\\MyKey', + '$keyFromIterator', + "'insideFirstForeach'", + ], + [ + __DIR__ . '/data/foreach/object-type.php', + 'ObjectType\\MyValue', + '$valueFromIterator', + "'insideFirstForeach'", + ], + [ + __DIR__ . '/data/foreach/object-type.php', + 'ObjectType\\MyKey', + '$keyFromAggregate', + "'insideSecondForeach'", + ], + [ + __DIR__ . '/data/foreach/object-type.php', + 'ObjectType\\MyValue', + '$valueFromAggregate', + "'insideSecondForeach'", + ], + [ + __DIR__ . '/data/foreach/object-type.php', + 'mixed', + '$keyFromRecursiveAggregate', + "'insideThirdForeach'", + ], + [ + __DIR__ . '/data/foreach/object-type.php', + 'mixed', + '$valueFromRecursiveAggregate', + "'insideThirdForeach'", + ], + ]; + } + + /** + * @dataProvider dataForeachObjectType + */ + public function testForeachObjectType( + string $file, + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + $file, + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataArrayFunctions(): array + { + return [ + [ + '1', + '$integers[0]', + ], + [ + 'array{string, string, string}', + '$mappedStrings', + ], + [ + 'string', + '$mappedStrings[0]', + ], + [ + '1|2|3', + '$filteredIntegers[0]', + ], + [ + '*ERROR*', + '$filteredMixed[0]', + ], + [ + '123', + '$filteredMixed[1]', + ], + [ + 'non-empty-array<0|1|2, 1|2|3>', + '$uniquedIntegers', + ], + [ + '1|2|3', + '$uniquedIntegers[1]', + ], + [ + 'string', + '$reducedIntegersToString', + ], + [ + 'string|null', + '$reducedIntegersToStringWithNull', + ], + [ + 'string', + '$reducedIntegersToStringAnother', + ], + [ + 'null', + '$reducedToNull', + ], + [ + '1|string', + '$reducedIntegersToStringWithInt', + ], + [ + '1', + '$reducedToInt', + ], + [ + 'array{1, 2, 3}', + 'array_change_key_case($integers)', + ], + [ + PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + 'array_combine($array, $array2)', + ], + [ + 'array{1: 2}', + 'array_combine([1], [2])', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + 'array_combine([1, 2], [3])', + ], + [ + 'array{a: \'d\', b: \'e\', c: \'f\'}', + 'array_combine([\'a\', \'b\', \'c\'], [\'d\', \'e\', \'f\'])', + ], + [ + PHP_VERSION_ID < 80000 ? 'array<1|2|3, mixed>|false' : 'array<1|2|3, mixed>', + 'array_combine([1, 2, 3], $array)', + ], + [ + PHP_VERSION_ID < 80000 ? 'array<1|2|3>|false' : 'array<1|2|3>', + 'array_combine($array, [1, 2, 3])', + ], + [ + 'array', + 'array_combine($array, $array)', + ], + [ + 'array', + 'array_combine($stringArray, $stringArray)', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_diff_assoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_diff_key($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_diff_uassoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_diff_ukey($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_diff($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_udiff_assoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_udiff_uassoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_udiff($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_intersect_assoc($integers, [])', + ], + [ + 'array{}', + 'array_intersect_key($integers, [])', + ], + [ + 'array{1, 2, 3}|array{4, 5, 6}', + 'array_intersect_key(...[$integers, [4, 5, 6]])', + ], + [ + 'array', + 'array_intersect_key(...$generalIntegersInAnotherArray)', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_intersect_uassoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_intersect_ukey($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_intersect($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_uintersect_assoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_uintersect_uassoc($integers, [])', + ], + [ + 'array<0|1|2, 1|2|3>', + 'array_uintersect($integers, [])', + ], + [ + 'array{1, 1, 1, 1, 1}', + '$filledIntegers', + ], + [ + 'array{}', + '$emptyFilled', + ], + [ + 'array{1}', + '$filledIntegersWithKeys', + ], + [ + 'non-empty-list<\'foo\'>', + '$filledNonEmptyArray', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$filledAlwaysFalse', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$filledNegativeConstAlwaysFalse', + ], + [ + PHP_VERSION_ID < 80000 ? 'list<1>|false' : 'list<1>', + '$filledByMaybeNegativeRange', + ], + [ + 'non-empty-list<1>', + '$filledByPositiveRange', + ], + [ + 'array{1, 2}', + 'array_keys($integerKeys)', + ], + [ + 'array{\'foo\', \'bar\'}', + 'array_keys($stringKeys)', + ], + [ + 'array{\'foo\', 1}', + 'array_keys($stringOrIntegerKeys)', + ], + [ + 'list', + 'array_keys($generalStringKeys)', + ], + [ + 'array{\'foo\', stdClass}', + 'array_values($integerKeys)', + ], + [ + 'list', + 'array_values($generalStringKeys)', + ], + [ + 'array{foo: stdClass, 0: stdClass}', + 'array_merge($stringOrIntegerKeys)', + ], + [ + 'array', + 'array_merge($generalStringKeys, $generalDateTimeValues)', + ], + [ + 'non-empty-array<1|string, int|stdClass>', + 'array_merge($generalStringKeys, $stringOrIntegerKeys)', + ], + [ + 'non-empty-array<1|string, int|stdClass>', + 'array_merge($stringOrIntegerKeys, $generalStringKeys)', + ], + [ + 'array{foo: stdClass, bar: stdClass, 0: stdClass}', + 'array_merge($stringKeys, $stringOrIntegerKeys)', + ], + [ + "array{foo: 'foo', 0: stdClass, bar: stdClass}", + 'array_merge($stringOrIntegerKeys, $stringKeys)', + ], + [ + '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))', + ], + [ + 'array', + 'array_merge(...[$generalStringKeys, $generalDateTimeValues])', + ], + [ + 'array', + '$mergedInts', + ], + [ + 'array{5: \'banana\', 6: \'banana\', 7: \'banana\', 8: \'banana\', 9: \'banana\', 10: \'banana\'}', + 'array_fill(5, 6, \'banana\')', + ], + [ + 'non-empty-list<\'apple\'>', + 'array_fill(0, 101, \'apple\')', + ], + [ + 'array{-2: \'pear\', 0: \'pear\', 1: \'pear\', 2: \'pear\'}', + 'array_fill(-2, 4, \'pear\')', + ], + [ + 'non-empty-array', + 'array_fill($integer, 2, new \stdClass())', + ], + [ + PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + 'array_fill(2, $integer, new \stdClass())', + ], + [ + 'array', + 'array_fill_keys($generalStringKeys, new \stdClass())', + ], + [ + 'array{foo: \'banana\', 5: \'banana\', 10: \'banana\', bar: \'banana\'}', + 'array_fill_keys([\'foo\', 5, 10, \'bar\'], \'banana\')', + ], + [ + 'array', + '$mappedStringKeys', + ], + [ + 'array', + '$mappedStringKeysWithUnknownClosureType', + ], + [ + 'array', + '$mappedWrongArray', + ], + [ + 'array', + '$unknownArray', + ], + [ + '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}', + 'array_map(function (): \stdClass {}, $conditionalKeysArray)', + ], + [ + 'stdClass', + 'array_pop($stringKeys)', + ], + [ + 'non-empty-array&hasOffsetValue(\'baz\', stdClass)', + '$stdClassesWithIsset', + ], + [ + 'stdClass', + 'array_pop($stdClassesWithIsset)', + ], + [ + '\'foo\'', + 'array_shift($stringKeys)', + ], + [ + 'int|null', + 'array_pop($generalStringKeys)', + ], + [ + 'int|null', + 'array_shift($generalStringKeys)', + ], + [ + 'null', + 'array_pop([])', + ], + [ + 'null', + 'array_shift([])', + ], + [ + 'array{null, \'\', 1}', + '$constantArrayWithFalseyValues', + ], + [ + 'array{2: 1}', + '$constantTruthyValues', + ], + [ + 'array', + '$falsey', + ], + [ + 'array{}', + 'array_filter($falsey)', + ], + [ + 'array', + '$withFalsey', + ], + [ + 'array', + 'array_filter($withFalsey)', + ], + [ + 'array{a: 1}', + 'array_filter($union)', + ], + [ + 'array{0?: true, 1?: int|int<1, max>}', + 'array_filter($withPossiblyFalsey)', + ], + [ + '(array|null)', + 'array_filter($mixed)', + ], + [ + '1|\'foo\'|false', + 'array_search(new stdClass, $stringOrIntegerKeys, true)', + ], + [ + '\'foo\'', + 'array_search(\'foo\', $stringKeys, true)', + ], + [ + 'int|false', + 'array_search(new DateTimeImmutable(), $generalDateTimeValues, true)', + ], + [ + 'string|false', + 'array_search(9, $generalStringKeys, true)', + ], + [ + 'string|false', + 'array_search(9, $generalStringKeys, false)', + ], + [ + 'string|false', + 'array_search(9, $generalStringKeys)', + ], + [ + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', + 'array_search(999, $integer, true)', + ], + [ + 'false', + 'array_search(new stdClass, $generalStringKeys, true)', + ], + [ + 'int|string|false', + 'array_search($mixed, $array, true)', + ], + [ + 'int|string|false', + 'array_search($mixed, $array, false)', + ], + [ + '\'a\'|\'b\'|false', + 'array_search($string, [\'a\' => \'A\', \'b\' => \'B\'], true)', + ], + [ + 'false', + 'array_search($integer, [\'a\' => \'A\', \'b\' => \'B\'], true)', + ], + [ + '\'foo\'|false', + 'array_search($generalIntegerOrString, $stringKeys, true)', + ], + [ + 'int|false', + 'array_search($generalIntegerOrString, $generalArrayOfIntegersOrStrings, true)', + ], + [ + 'int|false', + 'array_search($generalIntegerOrString, $clonedConditionalArray, true)', + ], + [ + 'int|string|false', + 'array_search($generalIntegerOrString, $generalIntegerOrStringKeys, false)', + ], + [ + 'false', + 'array_search(\'id\', $generalIntegerOrStringKeys, true)', + ], + [ + 'int|string|false', + 'array_search(\'id\', $generalIntegerOrStringKeysMixedValues, true)', + ], + [ + '*ERROR*', + 'array_search(\'id\', doFoo() ? $generalIntegerOrStringKeys : false, true)', + ], + [ + '*ERROR*', + 'array_search(\'id\', doFoo() ? [] : false, true)', + ], + [ + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', + 'array_search(\'id\', false, true)', + ], + [ + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', + 'array_search(\'id\', false)', + ], + [ + 'int|string|false', + 'array_search(\'id\', $thisDoesNotExistAndIsMixed, true)', + ], + [ + 'int|string|false', + 'array_search(\'id\', doFoo() ? $thisDoesNotExistAndIsMixedInUnion : false, true)', + ], + [ + 'int|string|false', + 'array_search(1, $generalIntegers, true)', + ], + [ + 'int|string|false', + 'array_search(1, $generalIntegers, false)', + ], + [ + 'int|string|false', + 'array_search(1, $generalIntegers)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 0)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 1)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 1, null, true)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 1, 2)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 1, 2, true)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 1, -1)', + ], + [ + 'array', + 'array_slice($generalStringKeys, 1, -1, true)', + ], + [ + 'array', + 'array_slice($generalStringKeys, -2)', + ], + [ + 'array', + 'array_slice($generalStringKeys, -2, 1, true)', + ], + [ + 'array', + 'array_slice($unknownArray, 0)', + ], + [ + 'array', + 'array_slice($unknownArray, 1)', + ], + [ + 'array', + 'array_slice($unknownArray, 1, null, true)', + ], + [ + 'array', + 'array_slice($unknownArray, 1, 2)', + ], + [ + 'array', + 'array_slice($unknownArray, 1, 2, true)', + ], + [ + 'array', + 'array_slice($unknownArray, 1, -1)', + ], + [ + 'array', + 'array_slice($unknownArray, 1, -1, true)', + ], + [ + 'array', + 'array_slice($unknownArray, -2)', + ], + [ + 'array', + 'array_slice($unknownArray, -2, 1, true)', + ], + [ + 'array{0: bool, 1: int, 2: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, 0)', + ], + [ + 'array{0: int, 1: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, 1)', + ], + [ + 'array{1: int, 2: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, 1, null, true)', + ], + [ + 'array{0: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, 2, 3)', + ], + [ + 'array{2: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, 2, 3, true)', + ], + [ + 'array{int, \'\'}', + 'array_slice($withPossiblyFalsey, 1, -1)', + ], + [ + 'array{1: int, 2: \'\'}', + 'array_slice($withPossiblyFalsey, 1, -1, true)', + ], + [ + 'array{0: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, -2, null)', + ], + [ + 'array{2: \'\', a: 0}', + 'array_slice($withPossiblyFalsey, -2, null, true)', + ], + [ + 'array{0: \'\', a: 0}|array{baz: \'qux\'}', + 'array_slice($unionArrays, 1)', + ], + [ + 'array{a: 0}|array{baz: \'qux\'}', + 'array_slice($unionArrays, -1, null, true)', + ], + [ + 'array{0: \'foo\', 1: \'bar\', baz: \'qux\', 2: \'quux\', quuz: \'corge\', 3: \'grault\'}', + '$slicedOffset', + ], + [ + 'array{4: \'foo\', 1: \'bar\', baz: \'qux\', 0: \'quux\', quuz: \'corge\', 5: \'grault\'}', + '$slicedOffsetWithKeys', + ], + [ + '0|1', + 'key($mixedValues)', + ], + [ + 'int|null', + 'key($falsey)', + ], + [ + 'string|null', + 'key($generalStringKeys)', + ], + [ + 'int|string|null', + 'key($generalIntegerOrStringKeysMixedValues)', + ], + [ + '\'foo\'', + '$poppedFoo', + ], + [ + 'int', + 'array_rand([1 => 1, 2 => "2"])', + ], + [ + 'string', + 'array_rand(["a" => 1, "b" => "2"])', + ], + [ + 'int|string', + 'array_rand(["a" => 1, 2 => "b"])', + ], + [ + 'int|string', + 'array_rand([1 => 1, 2 => "b", $mixed => $mixed])', + ], + [ + 'int', + 'array_rand([1 => 1, 2 => "b"], 1)', + ], + [ + 'string', + 'array_rand(["a" => 1, "b" => "b"], 1)', + ], + [ + 'int|string', + 'array_rand(["a" => 1, 2 => "b"], 1)', + ], + [ + 'int|string', + 'array_rand([1 => 1, 2 => "b", $mixed => $mixed], 1)', + ], + [ + 'array', + 'array_rand([1 => 1, 2 => "b"], 2)', + ], + [ + 'array', + 'array_rand(["a" => 1, "b" => "b"], 2)', + ], + [ + 'array', + 'array_rand(["a" => 1, 2 => "b"], 2)', + ], + [ + 'array', + 'array_rand([1 => 1, 2 => "2", $mixed => $mixed], 2)', + ], + [ + 'array|int', + 'array_rand([1 => 1, 2 => "b"], $mixed)', + ], + [ + 'array|string', + 'array_rand(["a" => 1, "b" => "b"], $mixed)', + ], + [ + 'array|int|string', + 'array_rand(["a" => 1, 2 => "b"], $mixed)', + ], + [ + 'array|int|string', + 'array_rand([1 => 1, 2 => "b", $mixed => $mixed], $mixed)', + ], + ]; + } + + /** + * @dataProvider dataArrayFunctions + */ + public function testArrayFunctions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-functions.php', + $description, + $expression, + ); + } + + 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', + '$microtimeStringWithoutArg', + ], + [ + 'string', + '$microtimeString', + ], + [ + 'float', + '$microtimeFloat', + ], + [ + 'float|string', + '$microtimeDefault', + ], + [ + '(float|string)', + '$microtimeBenevolent', + ], + [ + '-1', + '$versionCompare1', + ], + [ + '-1|1', + '$versionCompare2', + ], + [ + '-1|0|1', + '$versionCompare3', + ], + [ + '-1|0|1', + '$versionCompare4', + ], + [ + 'true', + '$versionCompare5', + ], + [ + 'bool', + '$versionCompare6', + ], + [ + 'bool', + '$versionCompare7', + ], + [ + 'bool', + '$versionCompare8', + ], + [ + 'string', + '$mbHttpOutputWithoutEncoding', + ], + [ + 'true', + '$mbHttpOutputWithValidEncoding', + ], + [ + 'false', + '$mbHttpOutputWithInvalidEncoding', + ], + [ + 'bool', + '$mbHttpOutputWithValidAndInvalidEncoding', + ], + [ + 'bool', + '$mbHttpOutputWithUnknownEncoding', + ], + [ + 'string', + '$mbRegexEncodingWithoutEncoding', + ], + [ + 'true', + '$mbRegexEncodingWithValidEncoding', + ], + [ + 'false', + '$mbRegexEncodingWithInvalidEncoding', + ], + [ + 'bool', + '$mbRegexEncodingWithValidAndInvalidEncoding', + ], + [ + 'bool', + '$mbRegexEncodingWithUnknownEncoding', + ], + [ + 'string', + '$mbInternalEncodingWithoutEncoding', + ], + [ + 'true', + '$mbInternalEncodingWithValidEncoding', + ], + [ + 'false', + '$mbInternalEncodingWithInvalidEncoding', + ], + [ + 'bool', + '$mbInternalEncodingWithValidAndInvalidEncoding', + ], + [ + 'bool', + '$mbInternalEncodingWithUnknownEncoding', + ], + [ + 'list', + '$mbEncodingAliasesWithValidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$mbEncodingAliasesWithInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbEncodingAliasesWithValidAndInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbEncodingAliasesWithUnknownEncoding', + ], + [ + 'string', + '$mbChrWithoutEncoding', + ], + [ + 'string', + '$mbChrWithValidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$mbChrWithInvalidEncoding', + ], + [ + 'string|false', + '$mbChrWithValidAndInvalidEncoding', + ], + [ + 'string|false', + '$mbChrWithUnknownEncoding', + ], + [ + 'int', + '$mbOrdWithoutEncoding', + ], + [ + 'int', + '$mbOrdWithValidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$mbOrdWithInvalidEncoding', + ], + [ + 'int|false', + '$mbOrdWithValidAndInvalidEncoding', + ], + [ + 'int|false', + '$mbOrdWithUnknownEncoding', + ], + [ + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}', + '$gettimeofdayArrayWithoutArg', + ], + [ + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}', + '$gettimeofdayArray', + ], + [ + 'float', + '$gettimeofdayFloat', + ], + [ + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}|float', + '$gettimeofdayDefault', + ], + [ + '(array{sec: int, usec: int, minuteswest: int, dsttime: int}|float)', + '$gettimeofdayBenevolent', + ], + [ + $strSplitDefaultReturnType, + '$strSplitConstantStringWithoutDefinedParameters', + ], + [ + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', + '$strSplitConstantStringWithoutDefinedSplitLength', + ], + [ + PHP_VERSION_ID < 80200 ? 'non-empty-list' : 'list', + '$strSplitStringWithoutDefinedSplitLength', + ], + [ + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', + '$strSplitConstantStringWithOneSplitLength', + ], + [ + 'array{\'abcdef\'}', + '$strSplitConstantStringWithGreaterSplitLengthThanStringLength', + ], + [ + 'false', + '$strSplitConstantStringWithFailureSplitLength', + ], + [ + $strSplitDefaultReturnType, + '$strSplitConstantStringWithInvalidSplitLengthType', + ], + [ + "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", + '$strSplitConstantStringWithVariableStringAndConstantSplitLength', + ], + [ + $strSplitDefaultReturnType, + '$strSplitConstantStringWithVariableStringAndVariableSplitLength', + ], + // parse_url + [ + 'array|int|string|false|null', + '$parseUrlWithoutParameters', + ], + [ + 'array{scheme: \'http\', host: \'abc.def\'}', + '$parseUrlConstantUrlWithoutComponent1', + ], + [ + 'array{scheme: \'http\', host: \'def.abc\'}', + '$parseUrlConstantUrlWithoutComponent2', + ], + [ + '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', + ], + [ + 'null', + '$parseUrlConstantUrlWithComponentNull', + ], + [ + "'this-is-fragment'", + '$parseUrlConstantUrlWithComponentSet', + ], + [ + 'false', + '$parseUrlConstantUrlWithComponentInvalid', + ], + [ + 'false', + '$parseUrlStringUrlWithComponentInvalid', + ], + [ + 'int<0, 65535>|false|null', + '$parseUrlStringUrlWithComponentPort', + ], + [ + 'array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', + '$parseUrlStringUrlWithoutComponent', + ], + [ + 'array{path: \'abc.def\'}', + "parse_url('http://github.com/abc.def')", + ], + [ + 'null', + "parse_url('http://github.com/abc.def',%20PHP_URL_SCHEME)", + ], + [ + "'http'", + "parse_url('http://abc.def/', PHP_URL_SCHEME)", + ], + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', + '$stat', + ], + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', + '$lstat', + ], + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', + '$fstat', + ], + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}', + '$fileObjectStat', + ], + [ + 'string', + '$base64DecodeWithoutStrict', + ], + [ + 'string', + '$base64DecodeWithStrictDisabled', + ], + [ + 'string|false', + '$base64DecodeWithStrictEnabled', + ], + [ + 'string', + '$base64DecodeDefault', + ], + [ + '(string|false)', + '$base64DecodeBenevolent', + ], + [ + '*ERROR*', + '$strWordCountWithoutParameters', + ], + [ + '*ERROR*', + '$strWordCountWithTooManyParams', + ], + [ + 'int', + '$strWordCountStr', + ], + [ + 'int', + '$strWordCountStrType0', + ], + [ + 'array', + '$strWordCountStrType1', + ], + [ + 'array', + '$strWordCountStrType1Extra', + ], + [ + 'array', + '$strWordCountStrType2', + ], + [ + 'array', + '$strWordCountStrType2Extra', + ], + [ + 'array|int|false', + '$strWordCountStrTypeIndeterminant', + ], + ]; + } + + /** + * @dataProvider dataFunctions + */ + public function testFunctions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/functions.php', + $description, + $expression, + ); + } + + public function dataDioFunctions(): array + { + return [ + [ + 'array{device: int, inode: int, mode: int, nlink: int, uid: int, gid: int, device_type: int, size: int, blocksize: int, blocks: int, atime: int, mtime: int, ctime: int}|null', + '$stat', + ], + ]; + } + + /** + * @dataProvider dataDioFunctions + */ + public function testDioFunctions( + string $description, + string $expression, + ): void + { + if (!function_exists('dio_stat')) { + $this->markTestSkipped('This test requires DIO extension.'); + } + $this->assertTypes( + __DIR__ . '/data/dio-functions.php', + $description, + $expression, + ); + } + + public function dataSsh2Functions(): array + { + return [ + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', + '$ssh2SftpStat', + ], + ]; + } + + /** + * @dataProvider dataSsh2Functions + */ + public function testSsh2Functions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/ssh2-functions.php', + $description, + $expression, + ); + } + + public function dataRangeFunction(): array + { + return [ + [ + 'array{2, 3, 4, 5}', + 'range(2, 5)', + ], + [ + 'array{2, 4}', + 'range(2, 5, 2)', + ], + [ + '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)', + ], + [ + 'array{2.1, 3.1, 4.1}', + 'range(2.1, 5)', + ], + [ + 'list', + 'range(2, 5, $integer)', + ], + [ + 'list', + 'range($float, 5, $integer)', + ], + [ + 'list<(float|int|string)>', + 'range($float, $mixed, $integer)', + ], + [ + 'list<(float|int|string)>', + 'range($integer, $mixed)', + ], + [ + 'array{0: 1, 1?: 2}', + 'range(1, doFoo() ? 1 : 2)', + ], + [ + 'array{0: -1, 1: 0, 2: 1, 3?: 2}|array{0: 1, 1?: 2}', + 'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)', + ], + [ + 'array{3, 2, 1, 0, -1}', + 'range(3, -1)', + ], + [ + 'non-empty-list>', + 'range(0, 50)', + ], + ]; + } + + /** + * @dataProvider dataRangeFunction + */ + public function testRangeFunction( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/range-function.php', + $description, + $expression, + ); + } + + public function dataSpecifiedTypesUsingIsFunctions(): array + { + return [ + [ + 'int', + '$integer', + ], + [ + 'int', + '$anotherInteger', + ], + [ + 'int', + '$longInteger', + ], + [ + 'float', + '$float', + ], + [ + 'float', + '$doubleFloat', + ], + [ + 'float', + '$realFloat', + ], + [ + 'null', + '$null', + ], + [ + 'array', + '$array', + ], + [ + 'bool', + '$bool', + ], + [ + 'callable(): mixed', + '$callable', + ], + [ + 'resource', + '$resource', + ], + [ + 'int', + '$yetAnotherInteger', + ], + [ + '*ERROR*', + '$mixedInteger', + ], + [ + 'string', + '$string', + ], + [ + 'object', + '$object', + ], + [ + 'int', + '$intOrStdClass', + ], + [ + 'Foo', + '$foo', + ], + [ + 'Foo', + '$anotherFoo', + ], + [ + 'class-string|Foo', + '$subClassOfFoo', + ], + [ + 'Foo', + '$subClassOfFoo2', + ], + [ + 'class-string|object', + '$subClassOfFoo3', + ], + [ + 'object', + '$subClassOfFoo4', + ], + [ + 'class-string|Foo', + '$subClassOfFoo5', + ], + [ + 'class-string|object', + '$subClassOfFoo6', + ], + [ + 'Foo', + '$subClassOfFoo7', + ], + [ + 'object', + '$subClassOfFoo8', + ], + [ + 'object', + '$subClassOfFoo9', + ], + [ + 'object', + '$subClassOfFoo10', + ], + [ + 'Foo', + '$subClassOfFoo11', + ], + [ + 'Foo', + '$subClassOfFoo12', + ], + [ + 'Foo', + '$subClassOfFoo13', + ], + [ + 'object', + '$subClassOfFoo14', + ], + [ + 'class-string|Foo', + '$subClassOfFoo15', + ], + [ + 'Bar|class-string|Foo', + '$subClassOfFoo16', + ], + ]; + } + + /** + * @dataProvider dataSpecifiedTypesUsingIsFunctions + */ + public function testSpecifiedTypesUsingIsFunctions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/specifiedTypesUsingIsFunctions.php', + $description, + $expression, + ); + } + + public function dataIterable(): array + { + return [ + [ + 'iterable', + '$this->iterableProperty', + ], + [ + 'iterable', + '$iterableSpecifiedLater', + ], + [ + 'iterable', + '$iterableWithoutTypehint', + ], + [ + 'mixed', + '$iterableWithoutTypehint[0]', + ], + [ + 'iterable', + '$iterableWithIterableTypehint', + ], + [ + 'mixed', + '$iterableWithIterableTypehint[0]', + ], + [ + 'mixed', + '$mixed', + ], + [ + 'iterable', + '$iterableWithConcreteTypehint', + ], + [ + 'mixed', + '$iterableWithConcreteTypehint[0]', + ], + [ + 'Iterables\Bar', + '$bar', + ], + [ + 'iterable', + '$this->doBar()', + ], + [ + 'iterable', + '$this->doBaz()', + ], + [ + 'Iterables\Baz', + '$baz', + ], + [ + 'array', + '$arrayWithIterableTypehint', + ], + [ + 'mixed', + '$arrayWithIterableTypehint[0]', + ], + [ + 'iterable&Iterables\Collection', + '$unionIterableType', + ], + [ + 'Iterables\Bar', + '$unionBar', + ], + [ + 'non-empty-array', + '$mixedUnionIterableType', + ], + [ + 'iterable&Iterables\Collection', + '$unionIterableIterableType', + ], + [ + 'mixed', + '$mixedBar', + ], + [ + 'Iterables\Bar', + '$iterableUnionBar', + ], + [ + 'Iterables\Bar', + '$unionBarFromMethod', + ], + [ + 'iterable', + '$this->stringIterableProperty', + ], + [ + 'iterable', + '$this->mixedIterableProperty', + ], + [ + 'iterable', + '$integers', + ], + [ + 'iterable', + '$mixeds', + ], + [ + 'iterable', + '$this->returnIterableMixed()', + ], + [ + 'iterable', + '$this->returnIterableString()', + ], + [ + 'int|iterable', + '$this->iterablePropertyAlsoWithSomethingElse', + ], + [ + 'int|iterable', + '$this->iterablePropertyWithTwoItemTypes', + ], + [ + 'array|Iterables\CollectionOfIntegers', + '$this->collectionOfIntegersOrArrayOfStrings', + ], + [ + 'Generator', + '$generatorOfFoos', + ], + [ + 'Iterables\Foo', + '$fooFromGenerator', + ], + [ + 'ArrayObject', + '$arrayObject', + ], + [ + 'int', + '$arrayObjectKey', + ], + [ + 'string', + '$arrayObjectValue', + ], + ]; + } + + /** + * @dataProvider dataIterable + */ + public function testIterable( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/iterable.php', + $description, + $expression, + ); + } + + public function dataArrayAccess(): array + { + return [ + [ + 'string', + '$this->returnArrayOfStrings()[0]', + ], + [ + 'mixed', + '$this->returnMixed()[0]', + ], + [ + 'int', + '$this->returnSelfWithIterableInt()[0]', + ], + [ + 'int', + '$this[0]', + ], + ]; + } + + /** + * @dataProvider dataArrayAccess + */ + public function testArrayAccess( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-accessable.php', + $description, + $expression, + ); + } + + public function dataVoid(): array + { + return [ + [ + 'null', + '$this->doFoo()', + ], + [ + 'null', + '$this->doBar()', + ], + [ + 'null', + '$this->doConflictingVoid()', + ], + ]; + } + + /** + * @dataProvider dataVoid + */ + public function testVoid( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/void.php', + $description, + $expression, + ); + } + + public function dataNullableReturnTypes(): array + { + return [ + [ + 'int|null', + '$this->doFoo()', + ], + [ + 'int|null', + '$this->doBar()', + ], + [ + 'int|null', + '$this->doConflictingNullable()', + ], + [ + 'int', + '$this->doAnotherConflictingNullable()', + ], + ]; + } + + /** + * @dataProvider dataNullableReturnTypes + */ + public function testNullableReturnTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/nullable-returnTypes.php', + $description, + $expression, + ); + } + + public function dataTernary(): array + { + return [ + [ + 'bool|null', + '$boolOrNull', + ], + [ + 'bool', + '$boolOrNull !== null ? $boolOrNull : false', + ], + [ + 'bool', + '$bool', + ], + [ + 'true|null', + '$short', + ], + [ + 'bool', + '$c', + ], + [ + 'bool', + '$isQux', + ], + ]; + } + + /** + * @dataProvider dataTernary + */ + public function testTernary( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/ternary.php', + $description, + $expression, + ); + } + + public function dataHeredoc(): array + { + return [ + [ + '\'foo\'', + '$heredoc', + ], + [ + '\'bar\'', + '$nowdoc', + ], + ]; + } + + /** + * @dataProvider dataHeredoc + */ + public function testHeredoc( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/heredoc.php', + $description, + $expression, + ); + } + + public function dataTypeElimination(): array + { + return [ + [ + 'null', + '$foo', + "'nullForSure'", + ], + [ + 'TypeElimination\Foo', + '$foo', + "'notNullForSure'", + ], + [ + 'TypeElimination\Foo', + '$foo', + "'notNullForSure2'", + ], + [ + 'null', + '$foo', + "'nullForSure2'", + ], + [ + 'null', + '$foo', + "'nullForSure3'", + ], + [ + 'TypeElimination\Foo', + '$foo', + "'notNullForSure3'", + ], + [ + 'null', + '$foo', + "'yodaNullForSure'", + ], + [ + 'TypeElimination\Foo', + '$foo', + "'yodaNotNullForSure'", + ], + [ + 'false', + '$intOrFalse', + "'falseForSure'", + ], + [ + 'int', + '$intOrFalse', + "'intForSure'", + ], + [ + 'false', + '$intOrFalse', + "'yodaFalseForSure'", + ], + [ + 'int', + '$intOrFalse', + "'yodaIntForSure'", + ], + [ + 'true', + '$intOrTrue', + "'trueForSure'", + ], + [ + 'int', + '$intOrTrue', + "'anotherIntForSure'", + ], + [ + 'true', + '$intOrTrue', + "'yodaTrueForSure'", + ], + [ + 'int', + '$intOrTrue', + "'yodaAnotherIntForSure'", + ], + [ + 'TypeElimination\Foo', + '$fooOrBarOrBaz', + "'fooForSure'", + ], + [ + 'TypeElimination\Bar|TypeElimination\Baz', + '$fooOrBarOrBaz', + "'barOrBazForSure'", + ], + [ + 'TypeElimination\Bar', + '$fooOrBarOrBaz', + "'barForSure'", + ], + [ + 'TypeElimination\Baz', + '$fooOrBarOrBaz', + "'bazForSure'", + ], + [ + 'TypeElimination\Bar|TypeElimination\Baz', + '$fooOrBarOrBaz', + "'anotherBarOrBazForSure'", + ], + [ + 'TypeElimination\Foo', + '$fooOrBarOrBaz', + "'anotherFooForSure'", + ], + [ + 'string|null', + '$result', + "'stringOrNullForSure'", + ], + [ + 'int', + '$intOrFalse', + "'yetAnotherIntForSure'", + ], + [ + 'int', + '$intOrTrue', + "'yetYetAnotherIntForSure'", + ], + [ + 'TypeElimination\Foo|null', + '$fooOrStringOrNull', + "'fooOrNull'", + ], + [ + 'string', + '$fooOrStringOrNull', + "'stringForSure'", + ], + [ + 'string', + '$fooOrStringOrNull', + "'anotherStringForSure'", + ], + [ + 'null', + '$this->bar', + "'propertyNullForSure'", + ], + [ + 'TypeElimination\Bar', + '$this->bar', + "'propertyNotNullForSure'", + ], + ]; + } + + /** + * @dataProvider dataTypeElimination + */ + public function testTypeElimination( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/type-elimination.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataMisleadingTypes(): array + { + return [ + [ + 'MisleadingTypes\boolean', + '$foo->misleadingBoolReturnType()', + ], + [ + 'MisleadingTypes\integer', + '$foo->misleadingIntReturnType()', + ], + [ + PHP_VERSION_ID >= 80000 ? 'mixed' : 'MisleadingTypes\mixed', + '$foo->misleadingMixedReturnType()', + ], + ]; + } + + /** + * @dataProvider dataMisleadingTypes + */ + public function testMisleadingTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/misleading-types.php', + $description, + $expression, + ); + } + + public function dataMisleadingTypesWithoutNamespace(): array + { + return [ + [ + 'boolean', // would have been "bool" for a real boolean + '$foo->misleadingBoolReturnType()', + ], + [ + 'integer', + '$foo->misleadingIntReturnType()', + ], + ]; + } + + /** + * @dataProvider dataMisleadingTypesWithoutNamespace + */ + public function testMisleadingTypesWithoutNamespace( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/misleading-types-without-namespace.php', + $description, + $expression, + ); + } + + public function dataUnresolvableTypes(): array + { + return [ + [ + 'mixed', + '$arrayWithTooManyArgs', + ], + [ + 'mixed', + '$iterableWithTooManyArgs', + ], + [ + 'Foo', + '$genericFoo', + ], + ]; + } + + /** + * @dataProvider dataUnresolvableTypes + */ + public function testUnresolvableTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/unresolvable-types.php', + $description, + $expression, + ); + } + + public function dataCombineTypes(): array + { + return [ + [ + 'string|null', + '$x', + ], + [ + '1|null', + '$y', + ], + ]; + } + + /** + * @dataProvider dataCombineTypes + */ + public function testCombineTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/combine-types.php', + $description, + $expression, + ); + } + + public function dataConstants(): array + { + define('ConstantsForNodeScopeResolverTest\\FOO_CONSTANT', 1); + + return [ + [ + '1', + '$foo', + ], + [ + '*ERROR*', + 'NONEXISTENT_CONSTANT', + ], + [ + "'bar'", + '\\BAR_CONSTANT', + ], + [ + 'mixed', + '\\BAZ_CONSTANT', + ], + ]; + } + + /** + * @dataProvider dataConstants + */ + public function testConstants( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/constants.php', + $description, + $expression, + ); + } + + public function dataFinally(): array + { + return [ + [ + '1|\'foo\'', + '$integerOrString', + ], + [ + 'FinallyNamespace\BarException|FinallyNamespace\FooException|null', + '$fooOrBarException', + ], + ]; + } + + /** + * @dataProvider dataFinally + */ + public function testFinally( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/finally.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataFinally + */ + public function testFinallyWithEarlyTermination( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/finally-with-early-termination.php', + $description, + $expression, + ); + } + + public function dataInheritDocFromInterface(): array + { + return [ + [ + 'string', + '$string', + ], + ]; + } + + /** + * @dataProvider dataInheritDocFromInterface + */ + public function testInheritDocFromInterface( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/inheritdoc-from-interface.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataInheritDocFromInterface + */ + public function testInheritDocWithoutCurlyBracesFromInterface( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface.php', + $description, + $expression, + ); + } + + public function dataInheritDocFromInterface2(): array + { + return [ + [ + 'int', + '$int', + ], + ]; + } + + /** + * @dataProvider dataInheritDocFromInterface2 + */ + public function testInheritDocFromInterface2( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/inheritdoc-from-interface2-definition.php'; + $this->assertTypes( + __DIR__ . '/data/inheritdoc-from-interface2.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataInheritDocFromInterface2 + */ + public function testInheritDocWithoutCurlyBracesFromInterface2( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2-definition.php'; + $this->assertTypes( + __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2.php', + $description, + $expression, + ); + } + + public function dataInheritDocFromTrait(): array + { + return [ + [ + 'string', + '$string', + ], + ]; + } + + /** + * @dataProvider dataInheritDocFromTrait + */ + public function testInheritDocFromTrait( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/inheritdoc-from-trait.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataInheritDocFromTrait + */ + public function testInheritDocWithoutCurlyBracesFromTrait( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait.php', + $description, + $expression, + ); + } + + public function dataInheritDocFromTrait2(): array + { + return [ + [ + 'string', + '$string', + ], + ]; + } + + /** + * @dataProvider dataInheritDocFromTrait2 + */ + public function testInheritDocFromTrait2( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/inheritdoc-from-trait2-definition.php'; + require_once __DIR__ . '/data/inheritdoc-from-trait2-definition2.php'; + $this->assertTypes( + __DIR__ . '/data/inheritdoc-from-trait2.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataInheritDocFromTrait2 + */ + public function testInheritDocWithoutCurlyBracesFromTrait2( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2-definition.php'; + require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2-definition2.php'; + $this->assertTypes( + __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2.php', + $description, + $expression, + ); + } + + public function dataResolveStatic(): array + { + return [ + [ + 'ResolveStatic\Foo', + '\ResolveStatic\Foo::create()', + ], + [ + 'ResolveStatic\Bar', + '\ResolveStatic\Bar::create()', + ], + [ + 'array{foo: ResolveStatic\\Bar}', + '$bar->returnConstantArray()', + ], + [ + 'ResolveStatic\Bar|null', + '$bar->nullabilityNotInSync()', + ], + [ + 'ResolveStatic\Bar', + '$bar->anotherNullabilityNotInSync()', + ], + ]; + } + + /** + * @dataProvider dataResolveStatic + */ + public function testResolveStatic( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/resolve-static.php', + $description, + $expression, + ); + } + + public function dataLoopVariables(): array + { + return [ + [ + 'LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'begin'", + ], + [ + 'LoopVariables\Foo', + '$foo', + "'afterAssign'", + ], + [ + 'LoopVariables\Foo', + '$foo', + "'end'", + ], + [ + 'int<1, max>|null', + '$nullableVal', + "'begin'", + ], + [ + 'null', + '$nullableVal', + "'nullableValIf'", + ], + [ + 'int<10, max>', + '$nullableVal', + "'nullableValElse'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'begin'", + ], + [ + 'LoopVariables\Foo', + '$falseOrObject', + "'end'", + ], + ]; + } + + public function dataForeachLoopVariables(): array + { + return [ + [ + '1|2|3', + '$val', + "'begin'", + ], + [ + '0|1|2', + '$key', + "'begin'", + ], + [ + '1|2|3|null', + '$val', + "'afterLoop'", + ], + [ + '0|1|2|null', + '$key', + "'afterLoop'", + ], + [ + '1|2|3|null', + '$emptyForeachVal', + "'afterLoop'", + ], + [ + '0|1|2|null', + '$emptyForeachKey', + "'afterLoop'", + ], + [ + '1|2|3', + '$nullableInt', + "'end'", + ], + [ + 'non-empty-list<1|2|3>', + '$integers', + "'end'", + ], + [ + 'list<1|2|3>', + '$integers', + "'afterLoop'", + ], + [ + 'array', + '$this->property', + "'begin'", + ], + [ + 'non-empty-array', + '$this->property', + "'end'", + ], + [ + 'array', + '$this->property', + "'afterLoop'", + ], + [ + 'int<0, max>', + '$i', + "'begin'", + ], + [ + 'int<0, max>', + '$i', + "'end'", + ], + [ + 'int<0, max>', + '$i', + "'afterLoop'", + ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>|null', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], + ]; + } + + public function dataWhileLoopVariables(): array + { + return [ + [ + 'int<1, 10>', + '$i', + "'begin'", + ], + [ + 'int<1, 10>', + '$i', + "'end'", + ], + [ + 'int<0, 10>', + '$i', + "'afterLoop'", + ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>|null', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], + ]; + } + + public function dataForLoopVariables(): array + { + return [ + [ + 'int<0, 9>', + '$i', + "'begin'", + ], + [ + 'int<0, 9>', + '$i', + "'end'", + ], + [ + 'int<0, max>', + '$i', + "'afterLoop'", + ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo', + '$falseOrObject', + "'afterLoop'", + ], + ]; + } + + /** + * @dataProvider dataLoopVariables + * @dataProvider dataForeachLoopVariables + */ + public function testForeachLoopVariables( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/foreach-loop-variables.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + /** + * @dataProvider dataLoopVariables + * @dataProvider dataWhileLoopVariables + */ + public function testWhileLoopVariables( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/while-loop-variables.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + /** + * @dataProvider dataLoopVariables + * @dataProvider dataForLoopVariables + */ + public function testForLoopVariables( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/for-loop-variables.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataDoWhileLoopVariables(): array + { + return [ + [ + 'LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'begin'", + ], + [ + 'LoopVariables\Foo', + '$foo', + "'afterAssign'", + ], + [ + 'LoopVariables\Foo', + '$foo', + "'end'", + ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem', + '$foo', + "'afterLoop'", + ], + [ + 'int<0, max>', + '$i', + "'begin'", + ], + [ + 'int<1, max>', + '$i', + "'end'", + ], + [ + 'int<0, max>', + '$i', + "'afterLoop'", + ], + [ + 'int<1, max>|null', + '$nullableVal', + "'begin'", + ], + [ + 'null', + '$nullableVal', + "'nullableValIf'", + ], + [ + 'int<10, max>', + '$nullableVal', + "'nullableValElse'", + ], + [ + '1|int<10, max>', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'begin'", + ], + [ + 'LoopVariables\Foo', + '$falseOrObject', + "'end'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$anotherFalseOrObject', + "'begin'", + ], + [ + 'LoopVariables\Foo', + '$anotherFalseOrObject', + "'end'", + ], + [ + 'LoopVariables\Foo', + '$anotherFalseOrObject', + "'afterLoop'", + ], + + ]; + } + + /** + * @dataProvider dataDoWhileLoopVariables + */ + public function testDoWhileLoopVariables( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/do-while-loop-variables.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataMultipleClassesInOneFile(): array + { + return [ + [ + 'MultipleClasses\Foo', + '$self', + "'Foo'", + ], + [ + 'MultipleClasses\Bar', + '$self', + "'Bar'", + ], + ]; + } + + /** + * @dataProvider dataMultipleClassesInOneFile + */ + public function testMultipleClassesInOneFile( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/multiple-classes-per-file.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataCallingMultipleClassesInOneFile(): array + { + return [ + [ + 'MultipleClasses\Foo', + '$foo->returnSelf()', + ], + [ + 'MultipleClasses\Bar', + '$bar->returnSelf()', + ], + ]; + } + + /** + * @dataProvider dataCallingMultipleClassesInOneFile + */ + public function testCallingMultipleClassesInOneFile( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/calling-multiple-classes-per-file.php', + $description, + $expression, + ); + } + + public function dataExplode(): array + { + return [ + [ + 'non-empty-list', + '$sureArray', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$sureFalse', + ], + [ + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', + '$arrayOrFalse', + ], + [ + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', + '$anotherArrayOrFalse', + ], + [ + PHP_VERSION_ID < 80000 ? '(non-empty-list|false)' : 'non-empty-list', + '$benevolentArrayOrFalse', + ], + ]; + } + + /** + * @dataProvider dataExplode + */ + public function testExplode( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/explode.php', + $description, + $expression, + ); + } + + public function dataArrayPointerFunctions(): array + { + return [ + [ + 'mixed', + 'reset()', + ], + [ + 'stdClass|false', + 'reset($generalArray)', + ], + [ + 'mixed', + 'reset($somethingElse)', + ], + [ + 'false', + 'reset($emptyConstantArray)', + ], + [ + '1', + 'reset($constantArray)', + ], + [ + '\'baz\'|\'foo\'', + 'reset($conditionalArray)', + ], + [ + '0|1', + 'reset($constantArrayOptionalKeys1)', + ], + [ + '0', + 'reset($constantArrayOptionalKeys2)', + ], + [ + '0', + 'reset($constantArrayOptionalKeys3)', + ], + [ + 'mixed', + 'end()', + ], + [ + 'stdClass|false', + 'end($generalArray)', + ], + [ + 'mixed', + 'end($somethingElse)', + ], + [ + 'false', + 'end($emptyConstantArray)', + ], + [ + '2', + 'end($constantArray)', + ], + [ + '\'bar\'|\'baz\'', + 'end($secondConditionalArray)', + ], + [ + '2', + 'end($constantArrayOptionalKeys1)', + ], + [ + '2', + 'end($constantArrayOptionalKeys2)', + ], + [ + '1|2', + 'end($constantArrayOptionalKeys3)', + ], + ]; + } + + /** + * @dataProvider dataArrayPointerFunctions + */ + public function testArrayPointerFunctions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-pointer-functions.php', + $description, + $expression, + ); + } + + public function dataReplaceFunctions(): array + { + return [ + [ + 'lowercase-string&non-falsy-string', + '$expectedString', + ], + [ + 'string|null', + '$expectedString2', + ], + [ + '(lowercase-string&non-falsy-string)|null', + '$anotherExpectedString', + ], + [ + 'array{a: string, b: string}', + '$expectedArray', + ], + [ + 'array{a?: string, b?: string}', + '$expectedArray2', + ], + [ + 'array{a?: string, b?: string}', + '$anotherExpectedArray', + ], + [ + 'list|string', + '$expectedArrayOrString', + ], + [ + '(list|string)', + '$expectedBenevolentArrayOrString', + ], + [ + 'list|string|null', + '$expectedArrayOrString2', + ], + [ + 'list|string|null', + '$anotherExpectedArrayOrString', + ], + [ + 'array{a?: string, b?: string}', + 'preg_replace_callback_array($callbacks, $array)', + ], + [ + 'string|null', + 'preg_replace_callback_array($callbacks, $string)', + ], + [ + 'string', + 'str_replace(\'.\', \':\', $intOrStringKey)', + ], + [ + 'string', + 'str_ireplace(\'.\', \':\', $intOrStringKey)', + ], + ]; + } + + /** + * @dataProvider dataReplaceFunctions + */ + public function testReplaceFunctions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/replaceFunctions.php', + $description, + $expression, + ); + } + + public function dataFilterVar(): Generator + { + $typesAndFilters = [ + 'string' => [ + 'FILTER_DEFAULT', + 'FILTER_UNSAFE_RAW', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_NUMBER_INT', + '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_URL', + ], + 'int' => ['FILTER_VALIDATE_INT'], + 'float' => ['FILTER_VALIDATE_FLOAT'], + ]; + + if (defined('FILTER_SANITIZE_MAGIC_QUOTES')) { + $typesAndFilters['string'][] = 'FILTER_SANITIZE_MAGIC_QUOTES'; + } + + if (defined('FILTER_SANITIZE_ADD_SLASHES')) { + $typesAndFilters['string'][] = 'FILTER_SANITIZE_ADD_SLASHES'; + } + + $typeAndFlags = [ + ['%s|false', ''], + ['%s|false', ', $mixed'], + ['%s|false', ', ["flags" => $mixed]'], + ['%s|null', ', FILTER_NULL_ON_FAILURE'], + ['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'], + ['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'], + ['%s|null', ', ["flags" => $nullFilter]'], + ['Analyser|%s', ', ["options" => ["default" => new Analyser]]'], + ['array<%s|null>', ', FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY'], + ['array<%s|null>', ', FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY | FILTER_FLAG_IPV4'], + ['array<%s|false>', ', FILTER_FORCE_ARRAY'], + ['array<%s|null>', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], + ['array<%s|false>', ', ["flags" => FILTER_FORCE_ARRAY | FILTER_FLAG_IPV4]'], + ['array<%s|false>', ', ["flags" => $forceArrayFilter]'], + ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_FORCE_ARRAY]'], + ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], + ]; + + foreach ($typesAndFilters as $filterType => $filters) { + foreach ($filters as $filter) { + foreach ($typeAndFlags as [$type, $flag]) { + yield [ + sprintf($type, $filterType), + sprintf('filter_var($mixed, %s %s)', $filter, $flag), + ]; + } + } + } + + $boolFlags = [ + ['bool', ''], + ['bool', ', $mixed'], + ['bool', ', ["flags" => $mixed]'], + ['bool|null', ', FILTER_NULL_ON_FAILURE'], + ['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'], + ['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'], + ['bool|null', ', ["flags" => $nullFilter]'], + ['Analyser|bool', ', ["options" => ["default" => new Analyser]]'], + ['bool', ', ["options" => ["default" => true]]'], + ['array', ', FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY'], + ['array', ', FILTER_FORCE_ARRAY'], + ['array', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], + ['array', ', ["flags" => $forceArrayFilter]'], + ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_FORCE_ARRAY]'], + ['array',', ["options" => ["default" => false], "flags" => FILTER_FORCE_ARRAY]'], + ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], + ]; + + foreach ($boolFlags as [$type, $flags]) { + yield [ + $type, + sprintf('filter_var($mixed, FILTER_VALIDATE_BOOLEAN %s)', $flags), + ]; + } + + //edge cases + yield 'unknown filter' => [ + 'mixed', + 'filter_var($mixed, $mixed)', + ]; + + yield 'default that is the same type as result' => [ + 'string', + 'filter_var($mixed, FILTER_SANITIZE_URL, ["options" => ["default" => "foo"]])', + ]; + + yield 'no second variable' => [ + 'string|false', + 'filter_var($mixed)', + ]; + } + + public function dataFilterVarUnchanged(): array + { + return [ + [ + '12', + 'filter_var(12, FILTER_VALIDATE_INT)', + ], + [ + 'false', + 'filter_var(false, FILTER_VALIDATE_BOOLEAN)', + ], + [ + 'array', + 'filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY)', + ], + [ + 'array', + 'filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE)', + ], + [ + '3.27', + 'filter_var(3.27, FILTER_VALIDATE_FLOAT)', + ], + [ + '3.27', + 'filter_var(3.27, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE)', + ], + [ + 'int<0, max>', + 'filter_var(rand(), FILTER_VALIDATE_INT)', + ], + [ + '12.0', + 'filter_var(12, FILTER_VALIDATE_FLOAT)', + ], + ]; + } + + /** + * @dataProvider dataFilterVar + * @dataProvider dataFilterVarUnchanged + */ + public function testFilterVar( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/filterVar.php', + $description, + $expression, + ); + } + + public function dataClosureWithUsePassedByReference(): array + { + return [ + [ + 'false', + '$progressStarted', + "'beforeCallback'", + ], + [ + 'false', + '$anotherVariable', + "'beforeCallback'", + ], + [ + '1|bool', + '$progressStarted', + "'inCallbackBeforeAssign'", + ], + [ + 'false', + '$anotherVariable', + "'inCallbackBeforeAssign'", + ], + [ + 'null', + '$untouchedPassedByRef', + "'inCallbackBeforeAssign'", + ], + [ + '1|true', + '$progressStarted', + "'inCallbackAfterAssign'", + ], + [ + 'true', + '$anotherVariable', + "'inCallbackAfterAssign'", + ], + [ + '1|bool', + '$progressStarted', + "'afterCallback'", + ], + [ + 'false', + '$anotherVariable', + "'afterCallback'", + ], + [ + 'null', + '$untouchedPassedByRef', + "'afterCallback'", + ], + [ + '1', + '$incrementedInside', + "'beforeCallback'", + ], + [ + 'int<1, max>', + '$incrementedInside', + "'inCallbackBeforeAssign'", + ], + [ + 'int<2, max>', + '$incrementedInside', + "'inCallbackAfterAssign'", + ], + [ + 'int<1, max>', + '$incrementedInside', + "'afterCallback'", + ], + [ + 'null', + '$fooOrNull', + "'beforeCallback'", + ], + [ + 'ClosurePassedByReference\Foo|null', + '$fooOrNull', + "'inCallbackBeforeAssign'", + ], + [ + 'ClosurePassedByReference\Foo', + '$fooOrNull', + "'inCallbackAfterAssign'", + ], + [ + 'ClosurePassedByReference\Foo|null', + '$fooOrNull', + "'afterCallback'", + ], + ]; + } + + /** + * @dataProvider dataClosureWithUsePassedByReference + */ + public function testClosureWithUsePassedByReference( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/closure-passed-by-reference.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataClosureWithUsePassedByReferenceInMethodCall(): array + { + return [ + [ + 'int|null', + '$five', + ], + ]; + } + + /** + * @dataProvider dataClosureWithUsePassedByReferenceInMethodCall + */ + public function testClosureWithUsePassedByReferenceInMethodCall( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/closure-passed-by-reference-in-call.php', + $description, + $expression, + ); + } + + public function dataClosureWithUsePassedByReferenceReturn(): array + { + return [ + [ + 'null', + '$fooOrNull', + "'beforeCallback'", + ], + [ + 'ClosurePassedByReference\Foo|null', + '$fooOrNull', + "'inCallbackBeforeAssign'", + ], + [ + 'ClosurePassedByReference\Foo', + '$fooOrNull', + "'inCallbackAfterAssign'", + ], + [ + 'ClosurePassedByReference\Foo|null', + '$fooOrNull', + "'afterCallback'", + ], + ]; + } + + public function dataStaticClosure(): array + { + return [ + [ + '*ERROR*', + '$this', + ], + ]; + } + + /** + * @dataProvider dataStaticClosure + */ + public function testStaticClosure( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/static-closure.php', + $description, + $expression, + ); + } + + /** + * @dataProvider dataClosureWithUsePassedByReferenceReturn + */ + public function testClosureWithUsePassedByReferenceReturn( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/closure-passed-by-reference-return.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataClosureWithInferredTypehint(): array + { + return [ + [ + 'DateTime|stdClass', + '$foo', + ], + [ + 'mixed', + '$bar', + ], + ]; + } + + /** + * @dataProvider dataClosureWithInferredTypehint + */ + public function testClosureWithInferredTypehint( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/closure-inferred-typehint.php', + $description, + $expression, + 'die', + [], + false, + ); + } + + public function dataTraitsPhpDocs(): array + { + return [ + [ + 'mixed', + '$this->propertyWithoutPhpDoc', + ], + [ + 'TraitPhpDocsTwo\TraitPropertyType', + '$this->traitProperty', + ], + [ + 'TraitPhpDocs\PropertyTypeFromClass', + '$this->conflictingProperty', + ], + /*[ + 'TraitPhpDocsTwo\AmbiguousPropertyType', + '$this->bogusProperty', + ],*/ + [ + 'TraitPhpDocs\BogusPropertyType', + '$this->anotherBogusProperty', + ], + [ + 'TraitPhpDocsTwo\BogusPropertyType', + '$this->differentBogusProperty', + ], + [ + 'string', + '$this->methodWithoutPhpDoc()', + ], + [ + 'TraitPhpDocsTwo\TraitMethodType', + '$this->traitMethod()', + ], + [ + 'TraitPhpDocs\MethodTypeFromClass', + '$this->conflictingMethod()', + ], + [ + 'TraitPhpDocs\AmbiguousMethodType', + '$this->bogusMethod()', + ], + [ + 'TraitPhpDocs\BogusMethodType', + '$this->anotherBogusMethod()', + ], + [ + 'TraitPhpDocsTwo\BogusMethodType', + '$this->differentBogusMethod()', + ], + [ + 'TraitPhpDocsTwo\DuplicateMethodType', + '$this->methodInMoreTraits()', + ], + [ + 'TraitPhpDocsThree\AnotherDuplicateMethodType', + '$this->anotherMethodInMoreTraits()', + ], + [ + 'TraitPhpDocsTwo\YetAnotherDuplicateMethodType', + '$this->yetAnotherMethodInMoreTraits()', + ], + [ + 'TraitPhpDocsThree\YetAnotherDuplicateMethodType', + '$this->aliasedYetAnotherMethodInMoreTraits()', + ], + [ + 'TraitPhpDocsThree\YetYetAnotherDuplicateMethodType', + '$this->yetYetAnotherMethodInMoreTraits()', + ], + [ + 'TraitPhpDocsTwo\YetYetAnotherDuplicateMethodType', + '$this->aliasedYetYetAnotherMethodInMoreTraits()', + ], + [ + 'int', + '$this->propertyFromTraitUsingTrait', + ], + [ + 'string', + '$this->methodFromTraitUsingTrait()', + ], + [ + 'TraitPhpDocsThree\Foo', + '$this->loremTraitProperty', + ], + ]; + } + + /** + * @dataProvider dataTraitsPhpDocs + */ + public function testTraitsPhpDocs( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/traits/traits.php', + $description, + $expression, + ); + } + + public function dataPassedByReference(): array + { + return [ + [ + 'array{1, 2, 3}', + '$arr', + ], + [ + 'array', + '$matches', + ], + [ + 'string', + '$s', + ], + ]; + } + + /** + * @dataProvider dataPassedByReference + */ + public function testPassedByReference( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/passed-by-reference.php', + $description, + $expression, + ); + } + + public function dataCallables(): array + { + return [ + [ + 'int', + '$foo()', + ], + [ + 'string', + '$closure()', + ], + [ + PHP_VERSION_ID < 80000 ? 'Callables\\Bar' : '*ERROR*', + '$arrayWithStaticMethod()', + ], + [ + PHP_VERSION_ID < 80000 ? 'float' : '*ERROR*', + '$stringWithStaticMethod()', + ], + [ + 'float', + '$arrayWithInstanceMethod()', + ], + [ + 'mixed', + '$closureObject()', + ], + ]; + } + + /** + * @dataProvider dataCallables + */ + public function testCallables( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/callables.php', + $description, + $expression, + ); + } + + public function dataArrayKeysInBranches(): array + { + return [ + [ + 'array{i: int<1, max>, j: int, k: int<1, max>, l: 1, m: 5, key: DateTimeImmutable, n?: \'str\'}', + '$array', + ], + [ + 'non-empty-array&hasOffsetValue(\'key\', mixed~null)', + '$generalArray', + ], + [ + 'mixed~null', + '$generalArray[\'key\']', + ], + [ + 'array{0: \'foo\', 1: \'bar\', 2?: \'baz\'}', + '$arrayAppendedInIf', + ], + [ + 'non-empty-list<\'bar\'|\'baz\'|\'foo\'>', + '$arrayAppendedInForeach', + ], + [ + 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' + '$anotherArrayAppendedInForeach', + ], + [ + '\'str\'', + '$array[\'n\']', + ], + [ + 'int<0, max>', + '$incremented', + ], + [ + '0|1', + '$setFromZeroToOne', + ], + ]; + } + + /** + * @dataProvider dataArrayKeysInBranches + */ + public function testArrayKeysInBranches( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-keys-branches.php', + $description, + $expression, + ); + } + + public function dataSpecifiedFunctionCall(): array + { + return [ + [ + 'true', + 'is_file($autoloadFile)', + "'first'", + ], + [ + 'true', + 'is_file($autoloadFile)', + "'second'", + ], + [ + 'true', + 'is_file($autoloadFile)', + "'third'", + ], + [ + 'bool', + 'is_file($autoloadFile)', + "'fourth'", + ], + [ + 'true', + 'is_file($autoloadFile)', + "'fifth'", + ], + ]; + } + + /** + * @dataProvider dataSpecifiedFunctionCall + */ + public function testSpecifiedFunctionCall( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/specified-function-call.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataElementsOnMixed(): array + { + return [ + [ + 'mixed', + '$mixed->foo', + ], + [ + 'mixed', + '$mixed->foo->bar', + ], + [ + 'mixed', + '$mixed->foo()', + ], + [ + 'mixed', + '$mixed->foo()->bar()', + ], + [ + 'mixed', + '$mixed::TEST_CONSTANT', + ], + ]; + } + + /** + * @dataProvider dataElementsOnMixed + */ + public function testElementsOnMixed( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/mixed-elements.php', + $description, + $expression, + ); + } + + public function dataCaseInsensitivePhpDocTypes(): array + { + return [ + [ + 'Foo\Bar', + '$this->bar', + ], + [ + 'Foo\Baz', + '$this->lorem', + ], + ]; + } + + /** + * @dataProvider dataCaseInsensitivePhpDocTypes + */ + public function testCaseInsensitivePhpDocTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/case-insensitive-phpdocs.php', + $description, + $expression, + ); + } + + public function dataConstantTypeAfterDuplicateCondition(): array + { + return [ + [ + '0', + '$a', + "'inCondition'", + ], + [ + '0', + '$b', + "'inCondition'", + ], + [ + '0', + '$c', + "'inCondition'", + ], + [ + 'int', + '$a', + "'afterFirst'", + ], + [ + 'int', + '$b', + "'afterFirst'", + ], + [ + '0', + '$c', + "'afterFirst'", + ], + [ + 'int|int<1, max>', + '$a', + "'afterSecond'", + ], + [ + 'int', + '$b', + "'afterSecond'", + ], + [ + '0', + '$c', + "'afterSecond'", + ], + [ + 'int|int<1, max>', + '$a', + "'afterThird'", + ], + [ + 'int|int<1, max>', + '$b', + "'afterThird'", + ], + [ + '0', + '$c', + "'afterThird'", + ], + ]; + } + + /** + * @dataProvider dataConstantTypeAfterDuplicateCondition + */ + public function testConstantTypeAfterDuplicateCondition( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/constant-types-duplicate-condition.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataAnonymousClass(): array + { + return [ + [ + '$this(AnonymousClass3301acd9e9d13ba9bbce9581cdb00699)', + '$this', + "'inside'", + ], + [ + 'AnonymousClass3301acd9e9d13ba9bbce9581cdb00699', + '$foo', + "'outside'", + ], + [ + 'AnonymousClassName\Foo', + '$this->fooProperty', + "'inside'", + ], + [ + 'AnonymousClassName\Foo', + '$foo->fooProperty', + "'outside'", + ], + [ + 'AnonymousClassName\Foo', + '$this->doFoo()', + "'inside'", + ], + [ + 'AnonymousClassName\Foo', + '$foo->doFoo()', + "'outside'", + ], + ]; + } + + /** + * @dataProvider dataAnonymousClass + */ + public function testAnonymousClassName( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/anonymous-class-name.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataAnonymousClassInTrait(): array + { + return [ + [ + '$this(AnonymousClass3de0a9734314db9dec21ba308363ff9a)', + '$this', + ], + ]; + } + + /** + * @dataProvider dataAnonymousClassInTrait + */ + public function testAnonymousClassNameInTrait( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/anonymous-class-name-in-trait.php', + $description, + $expression, + ); + } + + 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 [ + [ + 'string', + 'DynamicConstants\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', + ], + [ + "'abc123def'", + 'DynamicConstants\DynamicConstantClass::PURE_CONSTANT_IN_CLASS', + ], + [ + "'xyz'", + 'DynamicConstants\NoDynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', + ], + [ + 'false', + 'GLOBAL_DYNAMIC_CONSTANT', + ], + [ + '123', + 'GLOBAL_PURE_CONSTANT', + ], + ]; + } + + /** + * @dataProvider dataDynamicConstants + */ + public function testDynamicConstants( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/dynamic-constant.php', + $description, + $expression, + 'die', + [ + 'DynamicConstants\\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', + 'GLOBAL_DYNAMIC_CONSTANT', + ], + ); + } + + 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 [ + [ + '2|3', + '$array[\'b\']', + ], + [ + 'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}', + '$array', + ], + [ + 'array{a: 1, b: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}', + '$arrayCopy', + ], + [ + 'array{a: 2}', + '$anotherArrayCopy', + ], + [ + 'array{a: 1, b: 2}|array{a: 2}|array{a: 3, b: 3, c: 4}|array{a: 3, b: null}', + '$yetAnotherArrayCopy', + ], + [ + 'mixed~null', + '$mixedIsset', + ], + [ + 'non-empty-array&hasOffset(\'a\')', + '$mixedArrayKeyExists', + ], + [ + 'non-empty-array&hasOffsetValue(\'a\', int)', + '$integers', + ], + [ + 'int', + '$integers[\'a\']', + ], + [ + 'false', + '$lookup[\'derp\'] ?? false', + ], + [ + 'true', + '$lookup[\'foo\'] ?? false', + ], + [ + 'bool', + '$lookup[$a] ?? false', + ], + [ + '\'foo\'', + '$nullableArray[\'a\'] ?? false', + ], + [ + '\'bar\'', + '$nullableArray[\'b\'] ?? false', + ], + [ + '\'baz\'', + '$nullableArray[\'c\'] ?? false', + ], + ]; + } + + /** + * @dataProvider dataIsset + */ + public function testIsset( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/isset.php', + $description, + $expression, + ); + } + + public function dataPropertyArrayAssignment(): array + { + return [ + [ + 'mixed', + '$this->property', + "'start'", + ], + [ + 'array{}', + '$this->property', + "'emptyArray'", + ], + [ + '*ERROR*', + '$this->property[\'foo\']', + "'emptyArray'", + ], + [ + 'array{foo: 1}', + '$this->property', + "'afterAssignment'", + ], + [ + '1', + '$this->property[\'foo\']', + "'afterAssignment'", + ], + ]; + } + + /** + * @dataProvider dataPropertyArrayAssignment + */ + public function testPropertyArrayAssignment( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/property-array.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataGetParentClass(): array + { + return [ + [ + 'false', + 'get_parent_class()', + ], + [ + 'class-string|false', + 'get_parent_class($s)', + ], + [ + 'false', + 'get_parent_class(\ParentClass\Foo::class)', + ], + [ + 'class-string|false', + 'get_parent_class(NonexistentClass::class)', + ], + [ + 'class-string|false', + 'get_parent_class(1)', + ], + [ + "'ParentClass\\\\Foo'", + 'get_parent_class(\ParentClass\Bar::class)', + ], + [ + 'false', + 'get_parent_class()', + "'inParentClass'", + ], + [ + 'false', + 'get_parent_class($this)', + "'inParentClass'", + ], + [ + 'class-string', + 'get_class($this)', + "'inParentClass'", + ], + [ + '\'ParentClass\\\\Foo\'', + 'get_class()', + "'inParentClass'", + ], + [ + 'false', + 'get_class()', + ], + [ + "'ParentClass\\\\Foo'", + 'get_parent_class()', + "'inChildClass'", + ], + [ + "'ParentClass\\\\Foo'", + 'get_parent_class($this)', + "'inChildClass'", + ], + [ + 'class-string|false', + 'get_parent_class()', + "'inTrait'", + ], + [ + 'class-string|false', + 'get_parent_class($this)', + "'inTrait'", + ], + ]; + } + + /** + * @dataProvider dataGetParentClass + */ + public function testGetParentClass( + string $description, + string $expression, + string $evaluatedPointExpression = 'die', + ): void + { + $this->assertTypes( + __DIR__ . '/data/get-parent-class.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataIsCountable(): array + { + return [ + [ + 'array|Countable', + '$union', + "'is'", + ], + [ + 'string', + '$union', + "'is_not'", + ], + ]; + } + + /** + * @dataProvider dataIsCountable + */ + public function testIsCountable( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/is_countable.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + + public function dataPhp73Functions(): array + { + return [ + [ + 'non-empty-string|false', + 'json_encode($mixed)', + ], + [ + 'non-empty-string', + 'json_encode($mixed, JSON_THROW_ON_ERROR)', + ], + [ + 'non-empty-string', + 'json_encode($mixed, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], + [ + 'non-empty-string', + 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], + [ + 'mixed', + 'json_decode($mixed)', + ], + [ + 'mixed', + 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], + [ + 'mixed', + 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', + ], + [ + 'int|string|null', + 'array_key_first($mixedArray)', + ], + [ + 'int|string|null', + 'array_key_last($mixedArray)', + ], + [ + '(int|string)', + 'array_key_first($nonEmptyArray)', + ], + [ + '(int|string)', + 'array_key_last($nonEmptyArray)', + ], + [ + 'string|null', + 'array_key_first($arrayWithStringKeys)', + ], + [ + 'string|null', + 'array_key_last($arrayWithStringKeys)', + ], + [ + 'null', + 'array_key_first($emptyArray)', + ], + [ + 'null', + 'array_key_last($emptyArray)', + ], + [ + '0', + 'array_key_first($literalArray)', + ], + [ + '2', + 'array_key_last($literalArray)', + ], + [ + '0', + 'array_key_first($anotherLiteralArray)', + ], + [ + '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', + ], + [ + 'array{int, int}', + '$hrtime2', + ], + [ + '(float|int)', + '$hrtime3', + ], + [ + 'array{int, int}|float|int', + '$hrtime4', + ], + ]; + } + + /** + * @dataProvider dataPhp73Functions + */ + public function testPhp73Functions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/php73_functions.php', + $description, + $expression, + ); + } + + public function dataPhp74Functions(): array + { + return [ + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithoutDefinedParameters', + ], + [ + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', + '$mbStrSplitConstantStringWithoutDefinedSplitLength', + ], + [ + 'list', + '$mbStrSplitStringWithoutDefinedSplitLength', + ], + [ + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', + '$mbStrSplitConstantStringWithOneSplitLength', + ], + [ + 'array{\'abcdef\'}', + '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength', + ], + [ + 'false', + '$mbStrSplitConstantStringWithFailureSplitLength', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithInvalidSplitLengthType', + ], + [ + "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", + '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLength', + ], + [ + "array{'a', 'b', 'c', 'd', 'e', 'f'}", + '$mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding', + ], + [ + "array{'abcdef'}", + '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithFailureSplitLengthAndValidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithFailureSplitLengthAndInvalidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding', + ], + [ + "array{'a', 'b', 'c', 'd', 'e', 'f'}|array{'g', 'h', 'i', 'j', 'k', 'l'}", + '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding', + ], + [ + 'false', + '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding', + ], + [ + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding', + ], + ]; + } + + /** + * @dataProvider dataPhp74Functions + */ + public function testPhp74Functions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/php74_functions.php', + $description, + $expression, + ); + } + + public function dataUnionMethods(): array + { + return [ + [ + 'UnionMethods\Bar|UnionMethods\Foo', + '$something->doSomething()', + ], + [ + 'UnionMethods\Bar|UnionMethods\Foo', + '$something::doSomething()', + ], + ]; + } + + /** + * @dataProvider dataUnionMethods + */ + public function testUnionMethods( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/union-methods.php', + $description, + $expression, + ); + } + + public function dataUnionProperties(): array + { + return [ + [ + 'UnionProperties\Bar|UnionProperties\Foo', + '$something->doSomething', + ], + [ + 'UnionProperties\Bar|UnionProperties\Foo', + '$something::$doSomething', + ], + ]; + } + + /** + * @dataProvider dataUnionProperties + */ + public function testUnionProperties( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/union-properties.php', + $description, + $expression, + ); + } + + public function dataAssignmentInCondition(): array + { + return [ + [ + 'AssignmentInCondition\Foo', + '$bar', + ], + ]; + } + + /** + * @dataProvider dataAssignmentInCondition + */ + public function testAssignmentInCondition( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/assignment-in-condition.php', + $description, + $expression, + ); + } + + public function dataGeneralizeScope(): array + { + return [ + [ + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', + '$statistics', + ], + ]; + } + + /** + * @dataProvider dataGeneralizeScope + */ + public function testGeneralizeScope( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/generalize-scope.php', + $description, + $expression, + ); + } + + public function dataGeneralizeScopeRecursiveType(): array + { + return [ + [ + 'array{}|array{foo?: array}', + '$data', + ], + ]; + } + + /** + * @dataProvider dataGeneralizeScopeRecursiveType + */ + public function testGeneralizeScopeRecursiveType( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/generalize-scope-recursive.php', + $description, + $expression, + ); + } + + public function dataArrayShapesInPhpDoc(): array + { + return [ + [ + 'array{0: string, 1: ArrayShapesInPhpDoc\\Foo, foo: ArrayShapesInPhpDoc\\Bar, 2: ArrayShapesInPhpDoc\\Baz}', + '$one', + ], + [ + 'array{0: string, 1?: ArrayShapesInPhpDoc\\Foo, foo?: ArrayShapesInPhpDoc\\Bar}', + '$two', + ], + [ + 'array{0?: string, 1?: ArrayShapesInPhpDoc\\Foo, foo?: ArrayShapesInPhpDoc\\Bar}', + '$three', + ], + ]; + } + + /** + * @dataProvider dataArrayShapesInPhpDoc + */ + public function testArrayShapesInPhpDoc( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-shapes.php', + $description, + $expression, + ); + } + + public function dataInferPrivatePropertyTypeFromConstructor(): array + { + return [ + [ + 'int', + '$this->intProp', + ], + [ + 'string', + '$this->stringProp', + ], + [ + 'InferPrivatePropertyTypeFromConstructor\Bar|InferPrivatePropertyTypeFromConstructor\Foo', + '$this->unionProp', + ], + [ + 'stdClass', + '$this->stdClassProp', + ], + [ + 'stdClass', + '$this->unrelatedDocComment', + ], + [ + 'mixed', + '$this->explicitMixed', + ], + [ + 'bool', + '$this->bool', + ], + [ + 'array', + '$this->array', + ], + ]; + } + + /** + * @dataProvider dataInferPrivatePropertyTypeFromConstructor + */ + public function testInferPrivatePropertyTypeFromConstructor( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/infer-private-property-type-from-constructor.php', + $description, + $expression, + ); + } + + public function dataPropertyNativeTypes(): array + { + return [ + [ + 'string', + '$this->stringProp', + ], + [ + 'PropertyNativeTypes\Foo', + '$this->selfProp', + ], + [ + 'array', + '$this->integersProp', + ], + ]; + } + + /** + * @dataProvider dataPropertyNativeTypes + */ + public function testPropertyNativeTypes( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/property-native-types.php', + $description, + $expression, + ); + } + + public function dataArrowFunctions(): array + { + return [ + [ + 'Closure(string): 1', + '$x', + ], + [ + '1', + '$x()', + ], + [ + 'array{a: 1, b: 2}', + '$y()', + ], + ]; + } + + /** + * @dataProvider dataArrowFunctions + */ + public function testArrowFunctions( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/arrow-functions.php', + $description, + $expression, + ); + } + + public function dataArrowFunctionsInside(): array + { + return [ + [ + 'int', + '$i', + ], + [ + 'string', + '$s', + ], + [ + '*ERROR*', + '$t', + ], + ]; + } + + /** + * @dataProvider dataArrowFunctionsInside + */ + public function testArrowFunctionsInside( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/arrow-functions-inside.php', + $description, + $expression, + ); + } + + public function dataCoalesceAssign(): array + { + return [ + [ + 'string', + '$string ??= 1', + ], + [ + '1|string', + '$nullableString ??= 1', + ], + [ + '\'foo\'', + '$emptyArray[\'foo\'] ??= \'foo\'', + ], + [ + '\'foo\'', + '$arrayWithFoo[\'foo\'] ??= \'bar\'', + ], + [ + '\'bar\'|\'foo\'', + '$arrayWithMaybeFoo[\'foo\'] ??= \'bar\'', + ], + [ + 'array{foo: \'foo\'}', + '$arrayAfterAssignment', + ], + [ + 'array{foo: \'foo\'}', + '$arrayWithFooAfterAssignment', + ], + [ + '\'foo\'', + '$nonexistentVariableAfterAssignment', + ], + [ + '\'bar\'|\'foo\'', + '$maybeNonexistentVariableAfterAssignment', + ], + ]; + } + + /** + * @dataProvider dataCoalesceAssign + */ + public function testCoalesceAssign( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/coalesce-assign.php', + $description, + $expression, + ); + } + + public function dataArraySpread(): array + { + return [ + [ + 'non-empty-list', + '$integersOne', + ], + [ + 'non-empty-list', + '$integersTwo', + ], + [ + 'array{1, 2, 3, 4, 5, 6, 7}', + '$integersThree', + ], + [ + 'non-empty-list', + '$integersFour', + ], + [ + 'non-empty-list', + '$integersFive', + ], + [ + 'array{1, 2, 3, 4, 5, 6, 7}', + '$integersSix', + ], + [ + 'array{1, 2, 3, 4, 5, 6, 7}', + '$integersSeven', + ], + ]; + } + + /** + * @dataProvider dataArraySpread + */ + public function testArraySpread( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/array-spread.php', + $description, + $expression, + ); + } + + public function dataPhp74FunctionsIn74(): array + { + return [ + [ + 'list', + 'password_algos()', + ], + ]; + } + + /** + * @dataProvider dataPhp74FunctionsIn74 + */ + public function testPhp74FunctionsIn74( + string $description, + string $expression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/die-74.php', + $description, + $expression, + ); + } + + public function dataTryCatchScope(): array + { + return [ + [ + 'TryCatchScope\Foo', + '$resource', + "'first'", + ], + [ + 'TryCatchScope\Foo|null', + '$resource', + "'second'", + ], + [ + 'TryCatchScope\Foo|null', + '$resource', + "'third'", + ], + ]; + } + + /** + * @dataProvider dataTryCatchScope + */ + public function testTryCatchScope( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/try-catch-scope.php', + $description, + $expression, + $evaluatedPointExpression, + [], + false, + ); + } + + /** + * @param string[] $dynamicConstantNames + */ + private function assertTypes( + string $file, + string $description, + string $expression, + string $evaluatedPointExpression = 'die', + array $dynamicConstantNames = [], + bool $useCache = true, + ): void + { + $assertType = function (Scope $scope) use ($expression, $description, $evaluatedPointExpression): void { + /** @var Node\Stmt\Expression $expressionNode */ + $expressionNode = $this->getParser()->parseString(sprintf('getType($expressionNode->expr); + $this->assertTypeDescribe( + $description, + $type, + sprintf('%s at %s', $expression, $evaluatedPointExpression), + ); + }; + if ($useCache && isset(self::$assertTypesCache[$file][$evaluatedPointExpression])) { + $assertType(self::$assertTypesCache[$file][$evaluatedPointExpression]); + return; + } + + 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 + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/typeAliases.neon', + ]; + } + + public function dataDeclareStrictTypes(): array + { + return [ + [ + __DIR__ . '/data/declareWeakTypes.php', + false, + ], + [ + __DIR__ . '/data/noDeclare.php', + false, + ], + [ + __DIR__ . '/data/declareStrictTypes.php', + true, + ], + ]; + } + + /** + * @dataProvider dataDeclareStrictTypes + */ + public function testDeclareStrictTypes(string $file, bool $result): void + { + self::processFile($file, function (Node $node, Scope $scope) use ($result): void { + if (!($node instanceof Exit_)) { + return; + } + + $this->assertSame($result, $scope->isDeclareStrictTypes()); + }); + } + + public function testEarlyTermination(): void + { + self::processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { + if (!($node instanceof Exit_)) { + return; + } + + $this->assertTrue($scope->hasVariableType('something')->yes()); + $this->assertTrue($scope->hasVariableType('var')->yes()); + $this->assertTrue($scope->hasVariableType('foo')->no()); + }); + } + + protected static function getEarlyTerminatingMethodCalls(): array + { + return [ + \EarlyTermination\Foo::class => [ + 'doFoo', + 'doBar', + ], + ]; + } + + protected static function getEarlyTerminatingFunctionCalls(): array + { + return ['baz']; + } + + private function assertTypeDescribe( + string $expectedDescription, + Type $actualType, + string $label = '', + ): void + { + $actualDescription = $actualType->describe(VerbosityLevel::precise()); + $this->assertSame( + $expectedDescription, + $actualDescription, + $label, + ); + } + + /** @return string[] */ + protected static function getAdditionalAnalysedFiles(): array + { + return [ + __DIR__ . '/data/methodPhpDocs-trait-defined.php', + __DIR__ . '/data/anonymous-class-name-in-trait-trait.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 d61bbd8155..2bfafc8404 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -2,10686 +2,285 @@ namespace PHPStan\Analyser; -use Generator; -use PhpParser\Node; -use PhpParser\Node\Expr\Exit_; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; -use PhpParser\Node\Name; -use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Broker\Broker; -use PHPStan\Cache\Cache; +use EnumTypeAssertions\Foo; use PHPStan\File\FileHelper; -use PHPStan\File\SimpleRelativePathHelper; -use PHPStan\Node\VirtualNode; -use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; -use PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension; -use PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension; -use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantFloatType; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; -use SomeNodeScopeResolverNamespace\Foo; - -class NodeScopeResolverTest extends \PHPStan\Testing\TestCase -{ - - /** @var bool */ - private $polluteCatchScopeWithTryAssignments = true; - - /** @var Scope[][] */ - private static $assertTypesCache = []; - - public function testClassMethodScope(): void - { - $this->processFile(__DIR__ . '/data/class.php', function (\PhpParser\Node $node, Scope $scope): void { - if (!($node instanceof Exit_)) { - return; - } - - $this->assertSame('SomeNodeScopeResolverNamespace', $scope->getNamespace()); - $this->assertTrue($scope->isInClass()); - $this->assertSame(Foo::class, $scope->getClassReflection()->getName()); - $this->assertSame('doFoo', $scope->getFunctionName()); - $this->assertSame('$this(SomeNodeScopeResolverNamespace\Foo)', $scope->getVariableType('this')->describe(VerbosityLevel::precise())); - $this->assertTrue($scope->hasVariableType('baz')->yes()); - $this->assertTrue($scope->hasVariableType('lorem')->yes()); - $this->assertFalse($scope->hasVariableType('ipsum')->yes()); - $this->assertTrue($scope->hasVariableType('i')->yes()); - $this->assertTrue($scope->hasVariableType('val')->yes()); - $this->assertSame('SomeNodeScopeResolverNamespace\InvalidArgumentException', $scope->getVariableType('exception')->describe(VerbosityLevel::precise())); - $this->assertTrue($scope->hasVariableType('staticVariable')->yes()); - $this->assertSame($scope->getVariableType('staticVariable')->describe(VerbosityLevel::precise()), 'mixed'); - $this->assertTrue($scope->hasVariableType('staticVariableWithPhpDocType')->yes()); - $this->assertSame($scope->getVariableType('staticVariableWithPhpDocType')->describe(VerbosityLevel::precise()), 'string'); - $this->assertTrue($scope->hasVariableType('staticVariableWithPhpDocType2')->yes()); - $this->assertSame($scope->getVariableType('staticVariableWithPhpDocType2')->describe(VerbosityLevel::precise()), 'int'); - $this->assertTrue($scope->hasVariableType('staticVariableWithPhpDocType3')->yes()); - $this->assertSame($scope->getVariableType('staticVariableWithPhpDocType3')->describe(VerbosityLevel::precise()), 'float'); - }); - } - - private function getFileScope(string $filename): Scope - { - /** @var \PHPStan\Analyser\Scope $testScope */ - $testScope = null; - $this->processFile($filename, static function (\PhpParser\Node $node, Scope $scope) use (&$testScope): void { - if (!($node instanceof Exit_)) { - return; - } - - $testScope = $scope; - }); - - return $testScope; - } - - public function dataUnionInCatch(): array - { - return [ - [ - 'CatchUnion\BarException|CatchUnion\FooException', - '$e', - ], - ]; - } - - /** - * @dataProvider dataUnionInCatch - * @param string $description - * @param string $expression - */ - public function testUnionInCatch( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/catch-union.php', - $description, - $expression - ); - } - - public function dataUnionAndIntersection(): array - { - return [ - [ - 'UnionIntersection\AnotherFoo|UnionIntersection\Foo', - '$this->union->foo', - ], - [ - 'UnionIntersection\Bar', - '$this->union->bar', - ], - [ - 'UnionIntersection\Foo', - '$foo->foo', - ], - [ - '*ERROR*', - '$foo->bar', - ], - [ - 'UnionIntersection\AnotherFoo|UnionIntersection\Foo', - '$this->union->doFoo()', - ], - [ - 'UnionIntersection\Bar', - '$this->union->doBar()', - ], - [ - 'UnionIntersection\Foo', - '$foo->doFoo()', - ], - [ - '*ERROR*', - '$foo->doBar()', - ], - [ - 'UnionIntersection\AnotherFoo&UnionIntersection\Foo', - '$foobar->doFoo()', - ], - [ - 'UnionIntersection\Bar', - '$foobar->doBar()', - ], - [ - '1', - '$this->union::FOO_CONSTANT', - ], - [ - '1', - '$this->union::BAR_CONSTANT', - ], - [ - '1', - '$foo::FOO_CONSTANT', - ], - [ - '*ERROR*', - '$foo::BAR_CONSTANT', - ], - [ - '1', - '$foobar::FOO_CONSTANT', - ], - [ - '1', - '$foobar::BAR_CONSTANT', - ], - [ - '\'foo\'', - 'self::IPSUM_CONSTANT', - ], - [ - 'array(1, 2, 3)', - 'parent::PARENT_CONSTANT', - ], - [ - 'UnionIntersection\Foo', - '$foo::doStaticFoo()', - ], - [ - '*ERROR*', - '$foo::doStaticBar()', - ], - [ - 'UnionIntersection\AnotherFoo&UnionIntersection\Foo', - '$foobar::doStaticFoo()', - ], - [ - 'UnionIntersection\Bar', - '$foobar::doStaticBar()', - ], - [ - 'UnionIntersection\AnotherFoo|UnionIntersection\Foo', - '$this->union::doStaticFoo()', - ], - [ - 'UnionIntersection\Bar', - '$this->union::doStaticBar()', - ], - [ - 'object', - '$this->objectUnion', - ], - [ - 'UnionIntersection\SomeInterface', - '$object', - ], - ]; - } - - /** - * @dataProvider dataUnionAndIntersection - * @param string $description - * @param string $expression - */ - public function testUnionAndIntersection( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/union-intersection.php', - $description, - $expression - ); - } - - public function dataAssignInIf(): array - { - $testScope = $this->getFileScope(__DIR__ . '/data/if.php'); - - return [ - [ - $testScope, - 'nonexistentVariable', - TrinaryLogic::createNo(), - ], - [ - $testScope, - 'foo', - TrinaryLogic::createMaybe(), - 'bool', // mixed? - ], - [ - $testScope, - 'lorem', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'callParameter', - TrinaryLogic::createYes(), - '3', - ], - [ - $testScope, - 'arrOne', - TrinaryLogic::createYes(), - 'array(\'one\')', - ], - [ - $testScope, - 'arrTwo', - TrinaryLogic::createYes(), - 'array(\'test\' => \'two\', 0 => Foo)', - ], - [ - $testScope, - 'arrThree', - TrinaryLogic::createYes(), - 'array(\'three\')', - ], - [ - $testScope, - 'inArray', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'i', - TrinaryLogic::createYes(), - 'int', - ], - [ - $testScope, - 'f', - TrinaryLogic::createMaybe(), - 'int', - ], - [ - $testScope, - 'anotherF', - TrinaryLogic::createYes(), - 'int', - ], - [ - $testScope, - 'matches', - TrinaryLogic::createYes(), - 'mixed', - ], - [ - $testScope, - 'anotherArray', - TrinaryLogic::createYes(), - 'array(\'test\' => array(\'another\'))', - ], - [ - $testScope, - 'ifVar', - TrinaryLogic::createYes(), - '1|2|3', - ], - [ - $testScope, - 'ifNotVar', - TrinaryLogic::createMaybe(), - '1|2', - ], - [ - $testScope, - 'ifNestedVar', - TrinaryLogic::createYes(), - '1|2|3', - ], - [ - $testScope, - 'ifNotNestedVar', - TrinaryLogic::createMaybe(), - '1|2|3', - ], - [ - $testScope, - 'variableOnlyInEarlyTerminatingElse', - TrinaryLogic::createNo(), - ], - [ - $testScope, - 'matches2', - TrinaryLogic::createMaybe(), - 'mixed', - ], - [ - $testScope, - 'inTry', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'matches3', - TrinaryLogic::createYes(), - 'mixed', - ], - [ - $testScope, - 'matches4', - TrinaryLogic::createMaybe(), - 'mixed', - ], - [ - $testScope, - 'issetFoo', - TrinaryLogic::createYes(), - 'Foo', - ], - [ - $testScope, - 'issetBar', - TrinaryLogic::createYes(), - 'mixed~null', - ], - [ - $testScope, - 'issetBaz', - TrinaryLogic::createYes(), - 'mixed~null', - ], - [ - $testScope, - 'doWhileVar', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'switchVar', - TrinaryLogic::createYes(), - '1|2|3|4', - ], - [ - $testScope, - 'noSwitchVar', - TrinaryLogic::createMaybe(), - '1', - ], - [ - $testScope, - 'anotherNoSwitchVar', - TrinaryLogic::createMaybe(), - '1', - ], - [ - $testScope, - 'inTryTwo', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'ternaryMatches', - TrinaryLogic::createYes(), - 'mixed', - ], - [ - $testScope, - 'previousI', - TrinaryLogic::createYes(), - '0|1', - ], - [ - $testScope, - 'previousJ', - TrinaryLogic::createYes(), - '0', - ], - [ - $testScope, - 'frame', - TrinaryLogic::createYes(), - 'mixed', - ], - [ - $testScope, - 'listOne', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'listTwo', - TrinaryLogic::createYes(), - '2', - ], - [ - $testScope, - 'e', - TrinaryLogic::createYes(), - 'Exception', - ], - [ - $testScope, - 'exception', - TrinaryLogic::createYes(), - 'Exception', - ], - [ - $testScope, - 'inTryNotInCatch', - TrinaryLogic::createMaybe(), - '1', - ], - [ - $testScope, - 'fooObjectFromTryCatch', - TrinaryLogic::createYes(), - 'InTryCatchFoo', - ], - [ - $testScope, - 'mixedVarFromTryCatch', - TrinaryLogic::createYes(), - '1.0|1', - ], - [ - $testScope, - 'nullableIntegerFromTryCatch', - TrinaryLogic::createYes(), - '1|null', - ], - [ - $testScope, - 'anotherNullableIntegerFromTryCatch', - TrinaryLogic::createYes(), - '1|null', - ], - [ - $testScope, - 'nullableIntegers', - TrinaryLogic::createYes(), - 'array(1, 2, 3, null)', - ], - [ - $testScope, - 'union', - TrinaryLogic::createYes(), - 'array(1, 2, 3, \'foo\')', - '1|2|3|\'foo\'', - ], - [ - $testScope, - 'trueOrFalse', - TrinaryLogic::createYes(), - 'bool', - ], - [ - $testScope, - 'falseOrTrue', - TrinaryLogic::createYes(), - 'bool', - ], - [ - $testScope, - 'true', - TrinaryLogic::createYes(), - 'true', - ], - [ - $testScope, - 'false', - TrinaryLogic::createYes(), - 'false', - ], - [ - $testScope, - 'trueOrFalseFromSwitch', - TrinaryLogic::createYes(), - 'bool', - ], - [ - $testScope, - 'trueOrFalseInSwitchWithDefault', - TrinaryLogic::createYes(), - 'bool', - ], - [ - $testScope, - 'trueOrFalseInSwitchInAllCases', - TrinaryLogic::createYes(), - 'bool', - ], - [ - $testScope, - 'trueOrFalseInSwitchInAllCasesWithDefault', - TrinaryLogic::createYes(), - 'bool', - ], - [ - $testScope, - 'trueOrFalseInSwitchInAllCasesWithDefaultCase', - TrinaryLogic::createYes(), - 'true', - ], - [ - $testScope, - 'variableDefinedInSwitchWithOtherCasesWithEarlyTermination', - TrinaryLogic::createYes(), - 'true', - ], - [ - $testScope, - 'anotherVariableDefinedInSwitchWithOtherCasesWithEarlyTermination', - TrinaryLogic::createYes(), - 'true', - ], - [ - $testScope, - 'variableDefinedOnlyInEarlyTerminatingSwitchCases', - TrinaryLogic::createNo(), - ], - [ - $testScope, - 'nullableTrueOrFalse', - TrinaryLogic::createYes(), - 'bool|null', - ], - [ - $testScope, - 'nonexistentVariableOutsideFor', - TrinaryLogic::createMaybe(), - '1', - ], - [ - $testScope, - 'integerOrNullFromFor', - TrinaryLogic::createYes(), - '1|null', - ], - [ - $testScope, - 'nonexistentVariableOutsideWhile', - TrinaryLogic::createMaybe(), - '1', - ], - [ - $testScope, - 'integerOrNullFromWhile', - TrinaryLogic::createYes(), - '1|null', - ], - [ - $testScope, - 'nonexistentVariableOutsideForeach', - TrinaryLogic::createMaybe(), - 'null', - ], - [ - $testScope, - 'integerOrNullFromForeach', - TrinaryLogic::createYes(), - '1|null', - ], - [ - $testScope, - 'notNullableString', - TrinaryLogic::createYes(), - 'string', - ], - [ - $testScope, - 'anotherNotNullableString', - TrinaryLogic::createYes(), - 'string', - ], - [ - $testScope, - 'notNullableObject', - TrinaryLogic::createYes(), - 'Foo', - ], - [ - $testScope, - 'nullableString', - TrinaryLogic::createYes(), - 'string|null', - ], - [ - $testScope, - 'alsoNotNullableString', - TrinaryLogic::createYes(), - 'string', - ], - [ - $testScope, - 'integerOrString', - TrinaryLogic::createYes(), - '\'str\'|int', - ], - [ - $testScope, - 'nullableIntegerAfterNeverCondition', - TrinaryLogic::createYes(), - 'int|null', - ], - [ - $testScope, - 'stillNullableInteger', - TrinaryLogic::createYes(), - '2|null', - ], - [ - $testScope, - 'arrayOfIntegers', - TrinaryLogic::createYes(), - 'array(1, 2, 3)', - ], - [ - $testScope, - 'arrayAccessObject', - TrinaryLogic::createYes(), - \ObjectWithArrayAccess\Foo::class, - ], - [ - $testScope, - 'width', - TrinaryLogic::createYes(), - '2.0', - ], - [ - $testScope, - 'someVariableThatWillGetOverrideInFinally', - TrinaryLogic::createYes(), - '\'foo\'', - ], - [ - $testScope, - 'maybeDefinedButLaterCertainlyDefined', - TrinaryLogic::createYes(), - '2|3', - ], - [ - $testScope, - 'mixed', - TrinaryLogic::createYes(), - 'mixed', // should be mixed~bool+1 - ], - [ - $testScope, - 'variableDefinedInSwitchWithoutEarlyTermination', - TrinaryLogic::createMaybe(), - 'false', - ], - [ - $testScope, - 'anotherVariableDefinedInSwitchWithoutEarlyTermination', - TrinaryLogic::createMaybe(), - 'bool', - ], - [ - $testScope, - 'alwaysDefinedFromSwitch', - TrinaryLogic::createYes(), - '1|null', - ], - [ - $testScope, - 'exceptionFromTryCatch', - TrinaryLogic::createYes(), - '(AnotherException&Throwable)|(Throwable&YetAnotherException)|null', - ], - [ - $testScope, - 'nullOverwrittenInSwitchToOne', - TrinaryLogic::createYes(), - '1', - ], - [ - $testScope, - 'variableFromSwitchShouldBeBool', - TrinaryLogic::createYes(), - 'bool', - ], - ]; - } - - /** - * @dataProvider dataAssignInIf - * @param \PHPStan\Analyser\Scope $scope - * @param string $variableName - * @param \PHPStan\TrinaryLogic $expectedCertainty - * @param string|null $typeDescription - * @param string|null $iterableValueTypeDescription - */ - public function testAssignInIf( - Scope $scope, - string $variableName, - TrinaryLogic $expectedCertainty, - ?string $typeDescription = null, - ?string $iterableValueTypeDescription = null - ): void - { - $this->assertVariables( - $scope, - $variableName, - $expectedCertainty, - $typeDescription, - $iterableValueTypeDescription - ); - } - - public function dataConstantTypes(): array - { - $testScope = $this->getFileScope(__DIR__ . '/data/constantTypes.php'); - - return [ - [ - $testScope, - 'postIncrement', - '2', - ], - [ - $testScope, - 'postDecrement', - '4', - ], - [ - $testScope, - 'preIncrement', - '2', - ], - [ - $testScope, - 'preDecrement', - '4', - ], - [ - $testScope, - 'literalArray', - 'array(\'a\' => 2, \'b\' => 4, \'c\' => 2, \'d\' => 4)', - ], - [ - $testScope, - 'nullIncremented', - '1', - ], - [ - $testScope, - 'nullDecremented', - 'null', - ], - [ - $testScope, - 'incrementInIf', - '1|2|3', - ], - [ - $testScope, - 'anotherIncrementInIf', - '2|3', - ], - [ - $testScope, - 'valueOverwrittenInIf', - '1|2', - ], - [ - $testScope, - 'incrementInForLoop', - 'int', - ], - [ - $testScope, - 'valueOverwrittenInForLoop', - '1|2', - ], - [ - $testScope, - 'arrayOverwrittenInForLoop', - 'array(\'a\' => int, \'b\' => \'bar\'|\'foo\')', - ], - [ - $testScope, - 'anotherValueOverwrittenInIf', - '5|10', - ], - [ - $testScope, - 'intProperty', - 'int', - ], - [ - $testScope, - 'staticIntProperty', - 'int', - ], - [ - $testScope, - 'anotherIntProperty', - '1|2', - ], - [ - $testScope, - 'anotherStaticIntProperty', - '1|2', - ], - [ - $testScope, - 'variableIncrementedInClosurePassedByReference', - 'int', - ], - [ - $testScope, - 'anotherVariableIncrementedInClosure', - '0', - ], - [ - $testScope, - 'yetAnotherVariableInClosurePassedByReference', - 'int', - ], - [ - $testScope, - 'variableIncrementedInFinally', - '1', - ], - ]; - } - - /** - * @dataProvider dataConstantTypes - * @param \PHPStan\Analyser\Scope $scope - * @param string $variableName - * @param string $typeDescription - */ - public function testConstantTypes( - Scope $scope, - string $variableName, - string $typeDescription - ): void - { - $this->assertVariables( - $scope, - $variableName, - TrinaryLogic::createYes(), - $typeDescription, - null - ); - } - - private function assertVariables( - Scope $scope, - string $variableName, - TrinaryLogic $expectedCertainty, - ?string $typeDescription = null, - ?string $iterableValueTypeDescription = null - ): void - { - $certainty = $scope->hasVariableType($variableName); - $this->assertTrue( - $expectedCertainty->equals($certainty), - sprintf( - 'Certainty of variable $%s is %s, expected %s', - $variableName, - $certainty->describe(), - $expectedCertainty->describe() - ) - ); - if (!$expectedCertainty->no()) { - if ($typeDescription === null) { - $this->fail(sprintf('Missing expected type for defined variable $%s.', $variableName)); - } - - $this->assertSame( - $typeDescription, - $scope->getVariableType($variableName)->describe(VerbosityLevel::precise()), - sprintf('Type of variable $%s does not match the expected one.', $variableName) - ); - - if ($iterableValueTypeDescription !== null) { - $this->assertSame( - $iterableValueTypeDescription, - $scope->getVariableType($variableName)->getIterableValueType()->describe(VerbosityLevel::precise()), - sprintf('Iterable value type of variable $%s does not match the expected one.', $variableName) - ); - } - } elseif ($typeDescription !== null) { - $this->fail( - sprintf( - 'No type should be asserted for an undefined variable $%s, %s given.', - $variableName, - $typeDescription - ) - ); - } - } - - public function dataArrayDestructuring(): array - { - return [ - [ - 'mixed', - '$a', - ], - [ - 'mixed', - '$b', - ], - [ - 'mixed', - '$c', - ], - [ - 'mixed', - '$aList', - ], - [ - 'mixed', - '$bList', - ], - [ - 'mixed', - '$cList', - ], - [ - '1', - '$int', - ], - [ - '\'foo\'', - '$string', - ], - [ - 'true', - '$bool', - ], - [ - '*ERROR*', - '$never', - ], - [ - '*ERROR*', - '$nestedNever', - ], - [ - '1', - '$intList', - ], - [ - '\'foo\'', - '$stringList', - ], - [ - 'true', - '$boolList', - ], - [ - '*ERROR*', - '$neverList', - ], - [ - '*ERROR*', - '$nestedNeverList', - ], - [ - '1', - '$foreachInt', - ], - [ - 'false', - '$foreachBool', - ], - [ - '*ERROR*', - '$foreachNever', - ], - [ - '*ERROR*', - '$foreachNestedNever', - ], - [ - '1', - '$foreachIntList', - ], - [ - 'false', - '$foreachBoolList', - ], - [ - '*ERROR*', - '$foreachNeverList', - ], - [ - '*ERROR*', - '$foreachNestedNeverList', - ], - [ - '1|4', - '$u1', - ], - [ - '2|\'bar\'', - '$u2', - ], - [ - '3', - '$u3', - ], - [ - '1|4', - '$foreachU1', - ], - [ - '2|\'bar\'', - '$foreachU2', - ], - [ - '3', - '$foreachU3', - ], - [ - 'string', - '$firstStringArray', - ], - [ - 'string', - '$secondStringArray', - ], - [ - 'string', - '$thirdStringArray', - ], - [ - 'string', - '$fourthStringArray', - ], - [ - 'string', - '$firstStringArrayList', - ], - [ - 'string', - '$secondStringArrayList', - ], - [ - 'string', - '$thirdStringArrayList', - ], - [ - 'string', - '$fourthStringArrayList', - ], - [ - 'string', - '$firstStringArrayForeach', - ], - [ - 'string', - '$secondStringArrayForeach', - ], - [ - 'string', - '$thirdStringArrayForeach', - ], - [ - 'string', - '$fourthStringArrayForeach', - ], - [ - 'string', - '$firstStringArrayForeachList', - ], - [ - 'string', - '$secondStringArrayForeachList', - ], - [ - 'string', - '$thirdStringArrayForeachList', - ], - [ - 'string', - '$fourthStringArrayForeachList', - ], - [ - 'string', - '$dateArray[\'Y\']', - ], - [ - 'string', - '$dateArray[\'m\']', - ], - [ - 'int', - '$dateArray[\'d\']', - ], - [ - 'string', - '$intArrayForRewritingFirstElement[0]', - ], - [ - 'int', - '$intArrayForRewritingFirstElement[1]', - ], - [ - '*ERROR*', - '$obj', - ], - [ - 'stdClass', - '$newArray[\'newKey\']', - ], - [ - 'true', - '$assocKey', - ], - [ - '\'foo\'', - '$assocFoo', - ], - [ - '1', - '$assocOne', - ], - [ - '*ERROR*', - '$assocNonExistent', - ], - [ - 'true', - '$dynamicAssocKey', - ], - [ - '\'123\'|true', - '$dynamicAssocStrings', - ], - [ - '1|\'123\'|\'foo\'|true', - '$dynamicAssocMixed', - ], - [ - 'true', - '$dynamicAssocKeyForeach', - ], - [ - '\'123\'|true', - '$dynamicAssocStringsForeach', - ], - [ - '1|\'123\'|\'foo\'|true', - '$dynamicAssocMixedForeach', - ], - [ - 'string', - '$stringFromIterable', - ], - [ - 'string', - '$stringWithVarAnnotation', - ], - [ - 'string', - '$stringWithVarAnnotationInForeach', - ], - ]; - } - - /** - * @dataProvider dataArrayDestructuring - * @param string $description - * @param string $expression - */ - public function testArrayDestructuring( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/array-destructuring.php', - $description, - $expression - ); - } - - public function dataParameterTypes(): array - { - return [ - [ - 'int', - '$integer', - ], - [ - 'bool', - '$boolean', - ], - [ - 'string', - '$string', - ], - [ - 'float', - '$float', - ], - [ - 'TypesNamespaceTypehints\Lorem', - '$loremObject', - ], - [ - 'mixed', - '$mixed', - ], - [ - 'array', - '$array', - ], - [ - 'bool|null', - '$isNullable', - ], - [ - 'TypesNamespaceTypehints\Lorem', - '$loremObjectRef', - ], - [ - 'TypesNamespaceTypehints\Bar', - '$barObject', - ], - [ - 'TypesNamespaceTypehints\Foo', - '$fooObject', - ], - [ - 'TypesNamespaceTypehints\Bar', - '$anotherBarObject', - ], - [ - 'callable(): mixed', - '$callable', - ], - [ - 'array', - '$variadicStrings', - ], - [ - 'string', - '$variadicStrings[0]', - ], - ]; - } - - /** - * @dataProvider dataParameterTypes - * @param string $typeClass - * @param string $expression - */ - public function testTypehints( - string $typeClass, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/typehints.php', - $typeClass, - $expression - ); - } - - public function dataAnonymousFunctionParameterTypes(): array - { - return [ - [ - 'int', - '$integer', - ], - [ - 'bool', - '$boolean', - ], - [ - 'string', - '$string', - ], - [ - 'float', - '$float', - ], - [ - 'TypesNamespaceTypehints\Lorem', - '$loremObject', - ], - [ - 'mixed', - '$mixed', - ], - [ - 'array', - '$array', - ], - [ - 'bool|null', - '$isNullable', - ], - [ - 'callable(): mixed', - '$callable', - ], - [ - 'TypesNamespaceTypehints\FooWithAnonymousFunction', - '$self', - ], - ]; - } - - /** - * @dataProvider dataAnonymousFunctionParameterTypes - * @param string $description - * @param string $expression - */ - public function testAnonymousFunctionTypehints( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/typehints-anonymous-function.php', - $description, - $expression - ); - } - - public function dataVarAnnotations(): array - { - return [ - [ - 'int', - '$integer', - ], - [ - 'bool', - '$boolean', - ], - [ - 'string', - '$string', - ], - [ - 'float', - '$float', - ], - [ - 'VarAnnotations\Lorem', - '$loremObject', - ], - [ - 'AnotherNamespace\Bar', - '$barObject', - ], - [ - 'mixed', - '$mixed', - ], - [ - 'array', - '$array', - ], - [ - 'bool|null', - '$isNullable', - ], - [ - 'callable(): mixed', - '$callable', - ], - [ - 'callable(int, ...string): void', - '$callableWithTypes', - ], - [ - 'Closure(int, ...string): void', - '$closureWithTypes', - ], - [ - 'VarAnnotations\Foo', - '$self', - ], - [ - 'float', - '$invalidInteger', - ], - [ - 'static(VarAnnotations\Foo)', - '$static', - ], - ]; - } - - /** - * @dataProvider dataVarAnnotations - * @param string $description - * @param string $expression - */ - public function testVarAnnotations( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/var-annotations.php', - $description, - $expression, - [], - [], - [], - [], - 'die', - [], - false - ); - } - - public function dataCasts(): array - { - return [ - [ - 'int', - '$castedInteger', - ], - [ - 'bool', - '$castedBoolean', - ], - [ - 'float', - '$castedFloat', - ], - [ - 'string', - '$castedString', - ], - [ - 'array', - '$castedArray', - ], - [ - 'stdClass', - '$castedObject', - ], - [ - 'TypesNamespaceCasts\Foo', - '$castedFoo', - ], - [ - 'stdClass|TypesNamespaceCasts\Foo', - '$castedArrayOrObject', - ], - [ - '0|1', - '(int) $bool', - ], - [ - '0.0|1.0', - '(float) $bool', - ], - [ - '*ERROR*', - '(int) $foo', - ], - [ - 'true', - '(bool) $foo', - ], - [ - '1', - '(int) true', - ], - [ - '0', - '(int) false', - ], - [ - '5', - '(int) 5.25', - ], - [ - '5.0', - '(float) 5', - ], - [ - '5', - '(int) "5"', - ], - [ - '5.0', - '(float) "5"', - ], - [ - '*ERROR*', - '(int) "blabla"', - ], - [ - '*ERROR*', - '(float) "blabla"', - ], - [ - '0', - '(int) null', - ], - [ - '0.0', - '(float) null', - ], - [ - 'int', - '(int) $str', - ], - [ - 'float', - '(float) $str', - ], - [ - 'array(\'\' . "\0" . \'TypesNamespaceCasts\\\\Foo\' . "\0" . \'foo\' => TypesNamespaceCasts\Foo, \'\' . "\0" . \'TypesNamespaceCasts\\\\Foo\' . "\0" . \'int\' => int, \'\' . "\0" . \'*\' . "\0" . \'protectedInt\' => int, \'publicInt\' => int, \'\' . "\0" . \'TypesNamespaceCasts\\\\Bar\' . "\0" . \'barProperty\' => TypesNamespaceCasts\Bar)', - '(array) $foo', - ], - [ - 'array(1, 2, 3)', - '(array) [1, 2, 3]', - ], - [ - 'array(1)', - '(array) 1', - ], - [ - 'array(1.0)', - '(array) 1.0', - ], - [ - 'array(true)', - '(array) true', - ], - [ - 'array(\'blabla\')', - '(array) "blabla"', - ], - [ - 'array(int)', - '(array) $castedInteger', - ], - [ - 'array', - '(array) $iterable', - ], - [ - 'array', - '(array) new stdClass()', - ], - ]; - } - - /** - * @dataProvider dataCasts - * @param string $desciptiion - * @param string $expression - */ - public function testCasts( - string $desciptiion, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/casts.php', - $desciptiion, - $expression - ); - } - - public function dataUnsetCast(): array - { - return [ - [ - 'null', - '$castedNull', - ], - ]; - } - - /** - * @dataProvider dataUnsetCast - * @param string $desciptiion - * @param string $expression - */ - 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 [ - [ - '1', - '$integerLiteral', - ], - [ - 'true', - '$booleanLiteral', - ], - [ - 'false', - '$anotherBooleanLiteral', - ], - [ - '\'foo\'', - '$stringLiteral', - ], - [ - '1.0', - '$floatLiteral', - ], - [ - '1.0', - '$floatAssignedByRef', - ], - [ - 'null', - '$nullLiteral', - ], - [ - 'TypesNamespaceDeductedTypes\Lorem', - '$loremObjectLiteral', - ], - [ - 'mixed', - '$mixedObjectLiteral', - ], - [ - 'static(TypesNamespaceDeductedTypes\Foo)', - '$newStatic', - ], - [ - 'array()', - '$arrayLiteral', - ], - [ - 'string', - '$stringFromFunction', - ], - [ - 'TypesNamespaceFunctions\Foo', - '$fooObjectFromFunction', - ], - [ - 'mixed', - '$mixedFromFunction', - ], - [ - '1', - '\TypesNamespaceDeductedTypes\Foo::INTEGER_CONSTANT', - ], - [ - '1', - 'self::INTEGER_CONSTANT', - ], - [ - '1.0', - 'self::FLOAT_CONSTANT', - ], - [ - '\'foo\'', - 'self::STRING_CONSTANT', - ], - [ - 'array()', - 'self::ARRAY_CONSTANT', - ], - [ - 'true', - 'self::BOOLEAN_CONSTANT', - ], - [ - 'null', - 'self::NULL_CONSTANT', - ], - [ - '1', - '$foo::INTEGER_CONSTANT', - ], - [ - '1.0', - '$foo::FLOAT_CONSTANT', - ], - [ - '\'foo\'', - '$foo::STRING_CONSTANT', - ], - [ - 'array()', - '$foo::ARRAY_CONSTANT', - ], - [ - 'true', - '$foo::BOOLEAN_CONSTANT', - ], - [ - 'null', - '$foo::NULL_CONSTANT', - ], - ]; - } - - /** - * @dataProvider dataDeductedTypes - * @param string $description - * @param string $expression - */ - public function testDeductedTypes( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/function-definitions.php'; - $this->assertTypes( - __DIR__ . '/data/deducted-types.php', - $description, - $expression - ); - } - - public function dataProperties(): array - { - return [ - [ - 'mixed', - '$this->mixedProperty', - ], - [ - 'mixed', - '$this->anotherMixedProperty', - ], - [ - 'mixed', - '$this->yetAnotherMixedProperty', - ], - [ - 'int', - '$this->integerProperty', - ], - [ - 'int', - '$this->anotherIntegerProperty', - ], - [ - 'array', - '$this->arrayPropertyOne', - ], - [ - 'array', - '$this->arrayPropertyOther', - ], - [ - 'PropertiesNamespace\\Lorem', - '$this->objectRelative', - ], - [ - 'SomeOtherNamespace\\Ipsum', - '$this->objectFullyQualified', - ], - [ - 'SomeNamespace\\Amet', - '$this->objectUsed', - ], - [ - '*ERROR*', - '$this->nonexistentProperty', - ], - [ - 'int|null', - '$this->nullableInteger', - ], - [ - 'SomeNamespace\Amet|null', - '$this->nullableObject', - ], - [ - 'PropertiesNamespace\\Foo', - '$this->selfType', - ], - [ - 'static(PropertiesNamespace\Foo)', - '$this->staticType', - ], - [ - 'null', - '$this->nullType', - ], - [ - 'SomeNamespace\Sit', - '$this->inheritedProperty', - ], - [ - 'PropertiesNamespace\Bar', - '$this->barObject->doBar()', - ], - [ - 'mixed', - '$this->invalidTypeProperty', - ], - [ - 'resource', - '$this->resource', - ], - [ - 'array', - '$this->yetAnotherAnotherMixedParameter', - ], - [ - 'mixed', - '$this->yetAnotherAnotherAnotherMixedParameter', - ], - [ - 'string', - 'self::$staticStringProperty', - ], - [ - 'SomeGroupNamespace\One', - '$this->groupUseProperty', - ], - [ - 'SomeGroupNamespace\Two', - '$this->anotherGroupUseProperty', - ], - [ - 'PropertiesNamespace\Bar', - '$this->inheritDocProperty', - ], - [ - 'PropertiesNamespace\Bar', - '$this->inheritDocWithoutCurlyBracesProperty', - ], - [ - 'PropertiesNamespace\Bar', - '$this->implicitInheritDocProperty', - ], - [ - 'int', - '$this->readOnlyProperty', - ], - [ - 'string', - '$this->overriddenReadOnlyProperty', - ], - [ - 'string', - '$this->documentElement', - ], - ]; - } - - /** - * @dataProvider dataProperties - * @param string $description - * @param string $expression - */ - public function testProperties( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/properties.php', - $description, - $expression - ); - } - - public function dataBinaryOperations(): array - { - $typeCallback = static function ($value): string { - if (is_int($value)) { - return (new ConstantIntegerType($value))->describe(VerbosityLevel::precise()); - } elseif (is_float($value)) { - return (new ConstantFloatType($value))->describe(VerbosityLevel::precise()); - } elseif (is_bool($value)) { - return (new ConstantBooleanType($value))->describe(VerbosityLevel::precise()); - } elseif (is_string($value)) { - return (new ConstantStringType($value))->describe(VerbosityLevel::precise()); - } - - throw new \PHPStan\ShouldNotHappenException(); - }; - - return [ - [ - 'false', - 'true && false', - ], - [ - 'true', - 'true || false', - ], - [ - 'true', - 'true xor false', - ], - [ - 'true', - 'false xor true', - ], - [ - 'false', - 'true xor true', - ], - [ - 'false', - 'true xor true', - ], - [ - 'bool', - '$bool xor true', - ], - [ - 'bool', - '$bool xor false', - ], - [ - 'false', - 'true and false', - ], - [ - 'true', - 'true or false', - ], - [ - 'false', - '!true', - ], - [ - $typeCallback(-1), - '-1', - ], - [ - $typeCallback(+1), - '+1', - ], - [ - '*ERROR*', - '+"blabla"', - ], - [ - '123.2', - '+"123.2"', - ], - [ - '*ERROR*', - '-"blabla"', - ], - [ - '-5', - '-5', - ], - [ - '5', - '-(-5)', - ], - [ - 'int', - '-$integer', - ], - [ - '-2|-1', - '-$conditionalInt', - ], - [ - '*ERROR*', - '-$string', - ], - // integer + integer - [ - $typeCallback(1 + 1), - '1 + 1', - ], - [ - $typeCallback(1 - 1), - '1 - 1', - ], - [ - $typeCallback(1 / 2), - '1 / 2', - ], - [ - $typeCallback(1 * 1), - '1 * 1', - ], - [ - $typeCallback(1 ** 1), - '1 ** 1', - ], - [ - $typeCallback(1 % 1), - '1 % 1', - ], - [ - '(float|int)', - '$integer /= 2', - ], - [ - 'int', - '$integer *= 1', - ], - // float + float - [ - $typeCallback(1.2 + 1.4), - '1.2 + 1.4', - ], - [ - $typeCallback(1.2 - 1.4), - '1.2 - 1.4', - ], - [ - $typeCallback(1.2 / 2.4), - '1.2 / 2.4', - ], - [ - $typeCallback(1.2 * 1.4), - '1.2 * 1.4', - ], - [ - $typeCallback(1.2 ** 1.4), - '1.2 ** 1.4', - ], - [ - $typeCallback(3.2 % 2.4), - '3.2 % 2.4', - ], - [ - 'float', - '$float /= 2.4', - ], - [ - 'float', - '$float *= 2.4', - ], - // integer + float - [ - $typeCallback(1 + 1.4), - '1 + 1.4', - ], - [ - $typeCallback(1 - 1.4), - '1 - 1.4', - ], - [ - $typeCallback(1 / 2.4), - '1 / 2.4', - ], - [ - $typeCallback(1 * 1.4), - '1 * 1.4', - ], - [ - $typeCallback(1 ** 1.4), - '1 ** 1.4', - ], - [ - $typeCallback(3 % 2.4), - '3 % 2.4', - ], - [ - 'float', - '$integer /= 2.4', - ], - [ - 'float', - '$integer *= 2.4', - ], - [ - 'int', - '$otherInteger + 1', - ], - [ - 'float', - '$otherInteger + 1.0', - ], - // float + integer - [ - $typeCallback(1.2 + 1), - '1.2 + 1', - ], - [ - $typeCallback(1.2 - 1), - '1.2 - 1', - ], - [ - $typeCallback(1.2 / 2), - '1.2 / 2', - ], - [ - $typeCallback(1.2 * 1), - '1.2 * 1', - ], - [ - 'int', - '$integer * 10', - ], - [ - $typeCallback(1.2 ** 1), - '1.2 ** 1', - ], - [ - '(float|int)', - '$integer ** $integer', - ], - [ - $typeCallback(3.2 % 2), - '3.2 % 2', - ], - [ - 'int', - '$float %= 2.4', - ], - [ - 'float', - '$float **= 2.4', - ], - [ - 'float', - '$float /= 2.4', - ], - [ - 'float', - '$float *= 2', - ], - // boolean - [ - '1', - 'true + false', - ], - // string - [ - "'ab'", - "'a' . 'b'", - ], - [ - $typeCallback(1 . 'b'), - "1 . 'b'", - ], - [ - $typeCallback(1.0 . 'b'), - "1.0 . 'b'", - ], - [ - $typeCallback(1.0 . 2.0), - '1.0 . 2.0', - ], - [ - $typeCallback('foo' <=> 'bar'), - "'foo' <=> 'bar'", - ], - [ - '(float|int)', - '1 + $mixed', - ], - [ - 'float|int', - '1 + $number', - ], - [ - 'float|int', - '$integer + $number', - ], - [ - 'float', - '$float + $float', - ], - [ - 'float', - '$float + $number', - ], - [ - '(float|int)', - '1 / $mixed', - ], - [ - 'float|int', - '1 / $number', - ], - [ - 'float', - '1.0 / $mixed', - ], - [ - 'float', - '1.0 / $number', - ], - [ - '(float|int)', - '$mixed / 1', - ], - [ - 'float|int', - '$number / 1', - ], - [ - 'float', - '$mixed / 1.0', - ], - [ - 'float', - '$number / 1.0', - ], - [ - 'float', - '1.0 + $mixed', - ], - [ - 'float', - '1.0 + $number', - ], - [ - '(float|int)', - '$mixed + 1', - ], - [ - 'float|int', - '$number + 1', - ], - [ - 'float', - '$mixed + 1.0', - ], - [ - 'float', - '$number + 1.0', - ], - [ - '\'foo\'|null', - '$mixed ? "foo" : null', - ], - [ - '12', - '12 ?: null', - ], - [ - '1', - 'true ? 1 : 2', - ], - [ - '2', - 'false ? 1 : 2', - ], - [ - '12|string', - '$string ?: 12', - ], - [ - '12|string', - '$stringOrNull ?: 12', - ], - [ - '12|string', - '@$stringOrNull ?: 12', - ], - [ - 'int<1, max>|int', - '$integer ?: 12', - ], - [ - '\'foo\'', - "'foo' ?? null", // "else" never gets executed - ], - [ - 'string|null', - '$stringOrNull ?? null', - ], - [ - '\'bar\'|\'foo\'', - '$maybeDefinedVariable ?? \'bar\'', - ], - [ - 'string', - '$string ?? \'foo\'', - ], - [ - 'string', - '$stringOrNull ?? \'foo\'', - ], - [ - 'string', - '$string ?? $integer', - ], - [ - 'int|string', - '$stringOrNull ?? $integer', - ], - [ - '\'Foo\'', - '\Foo::class', - ], - [ - '74', - '$line', - ], - [ - (new ConstantStringType(__DIR__ . '/data'))->describe(VerbosityLevel::precise()), - '$dir', - ], - [ - (new ConstantStringType(__DIR__ . '/data/binary.php'))->describe(VerbosityLevel::precise()), - '$file', - ], - [ - '\'BinaryOperations\\\\NestedNamespace\'', - '$namespace', - ], - [ - '\'BinaryOperations\\\\NestedNamespace\\\\Foo\'', - '$class', - ], - [ - '\'BinaryOperations\\\\NestedNamespace\\\\Foo::doFoo\'', - '$method', - ], - [ - '\'doFoo\'', - '$function', - ], - [ - '1', - 'min([1, 2, 3])', - ], - [ - 'array(1, 2, 3)', - 'min([1, 2, 3], [4, 5, 5])', - ], - [ - '1', - 'min(...[1, 2, 3])', - ], - [ - '1', - 'min(...[2, 3, 4], ...[5, 1, 8])', - ], - [ - '0', - 'min(0, ...[1, 2, 3])', - ], - [ - 'array(5, 6, 9)', - 'max([1, 10, 8], [5, 6, 9])', - ], - [ - 'array(1, 1, 1, 1)', - 'max(array(2, 2, 2), array(1, 1, 1, 1))', - ], - [ - 'array', - 'max($arrayOfUnknownIntegers, $arrayOfUnknownIntegers)', - ], - /*[ - 'array(1, 1, 1, 1)', - 'max(array(2, 2, 2), 5, array(1, 1, 1, 1))', - ], - [ - 'array', - 'max($arrayOfUnknownIntegers, $integer, $arrayOfUnknownIntegers)', - ],*/ - [ - '1.1', - 'min(...[1.1, 2.2, 3.3])', - ], - [ - '1.1', - 'min(...[1.1, 2, 3])', - ], - [ - '3', - 'max(...[1, 2, 3])', - ], - [ - '3.3', - 'max(...[1.1, 2.2, 3.3])', - ], - [ - '1', - 'min(1, 2, 3)', - ], - [ - '3', - 'max(1, 2, 3)', - ], - [ - '1.1', - 'min(1.1, 2.2, 3.3)', - ], - [ - '3.3', - 'max(1.1, 2.2, 3.3)', - ], - [ - '1', - 'min(1, 1)', - ], - [ - '*ERROR*', - 'min(1)', - ], - [ - 'int|string', - 'min($integer, $string)', - ], - [ - 'int|string', - 'min([$integer, $string])', - ], - [ - 'int|string', - 'min(...[$integer, $string])', - ], - [ - '\'a\'', - 'min(\'a\', \'b\')', - ], - [ - 'DateTimeImmutable', - 'max(new \DateTimeImmutable("today"), new \DateTimeImmutable("tomorrow"))', - ], - [ - '1', - 'min(1, 2.2, 3.3)', - ], - [ - 'string', - '"Hello $world"', - ], - [ - 'string', - '$string .= "str"', - ], - [ - 'int', - '$integer <<= 2.2', - ], - [ - 'int', - '$float >>= 2.2', - ], - [ - '3', - 'count($arrayOfIntegers)', - ], - [ - 'int', - 'count($arrayOfIntegers, \COUNT_RECURSIVE)', - ], - [ - '3', - 'count($arrayOfIntegers, 5)', - ], - [ - '6', - 'count($arrayOfIntegers) + count($arrayOfIntegers)', - ], - [ - 'bool', - '$string === "foo"', - ], - [ - 'true', - '$fooString === "foo"', - ], - [ - 'bool', - '$string !== "foo"', - ], - [ - 'false', - '$fooString !== "foo"', - ], - [ - 'bool', - '$string == "foo"', - ], - [ - 'bool', - '$string != "foo"', - ], - [ - 'true', - '$foo instanceof \BinaryOperations\NestedNamespace\Foo', - ], - [ - 'bool', - '$foo instanceof Bar', - ], - [ - 'true', - 'isset($foo)', - ], - [ - 'true', - 'isset($foo, $one)', - ], - [ - 'false', - 'isset($null)', - ], - [ - 'false', - 'isset($undefinedVariable)', - ], - [ - 'false', - 'isset($foo, $undefinedVariable)', - ], - [ - 'bool', - 'isset($stringOrNull)', - ], - [ - 'false', - 'isset($stringOrNull, $null)', - ], - [ - 'false', - 'isset($stringOrNull, $undefinedVariable)', - ], - [ - 'bool', - 'isset($foo, $stringOrNull)', - ], - [ - 'bool', - 'isset($foo, $stringOrNull)', - ], - [ - 'true', - 'isset($array[\'0\'])', - ], - [ - 'bool', - 'isset($array[$integer])', - ], - [ - 'false', - 'isset($array[$integer], $array[1000])', - ], - [ - 'false', - 'isset($array[$integer], $null)', - ], - [ - 'bool', - 'isset($array[\'0\'], $array[$integer])', - ], - [ - 'bool', - 'isset($foo, $array[$integer])', - ], - [ - 'false', - 'isset($foo, $array[1000])', - ], - [ - 'false', - 'isset($foo, $array[1000])', - ], - [ - 'false', - '!isset($foo)', - ], - [ - 'bool', - 'empty($foo)', - ], - [ - 'bool', - '!empty($foo)', - ], - [ - 'array(int, int, int)', - '$arrayOfIntegers + $arrayOfIntegers', - ], - [ - 'array(int, int, int)', - '$arrayOfIntegers += $arrayOfIntegers', - ], - [ - 'array(0 => 1, 1 => 1, 2 => 1, 3 => 1|2, 4 => 1|3, ?5 => 2|3, ?6 => 3)', - '$conditionalArray + $unshiftedConditionalArray', - ], - [ - 'array(0 => \'lorem\', 1 => stdClass, 2 => 1, 3 => 1, 4 => 1, ?5 => 2|3, ?6 => 3)', - '$unshiftedConditionalArray + $conditionalArray', - ], - [ - 'array(int, int, int)', - '$arrayOfIntegers += ["foo"]', - ], - [ - '*ERROR*', - '$arrayOfIntegers += "foo"', - ], - [ - '3', - '@count($arrayOfIntegers)', - ], - [ - '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++', - ], - [ - '1', - '$one--', - ], - [ - '2', - '++$one', - ], - [ - '0', - '--$one', - ], - [ - '*ERROR*', - '$preIncArray[0]', - ], - [ - '1', - '$preIncArray[1]', - ], - [ - '2', - '$preIncArray[2]', - ], - [ - '*ERROR*', - '$preIncArray[3]', - ], - [ - 'array(1 => 1, 2 => 2)', - '$preIncArray', - ], - [ - 'array(0 => 1, 2 => 3)', - '$postIncArray', - ], - [ - 'array(0 => array(1 => array(2 => 3)), 4 => array(5 => array(6 => 7)))', - '$anotherPostIncArray', - ], - [ - '3', - 'count($array)', - ], - [ - 'int', - 'count()', - ], - [ - 'int', - 'count($appendingToArrayInBranches)', - ], - [ - '3|4|5', - 'count($conditionalArray)', - ], - [ - '2', - '$array[1]', - ], - [ - '(float|int)', - '$integer / $integer', - ], - [ - '(float|int)', - '$otherInteger / $integer', - ], - [ - '(array|float|int)', - '$mixed + $mixed', - ], - [ - '(float|int)', - '$mixed - $mixed', - ], - [ - '*ERROR*', - '$mixed + []', - ], - [ - '124', - '1 + "123"', - ], - [ - '124.2', - '1 + "123.2"', - ], - [ - '*ERROR*', - '1 + $string', - ], - [ - '*ERROR*', - '1 + "blabla"', - ], - [ - 'array(1, 2, 3)', - '[1, 2, 3] + [4, 5, 6]', - ], - [ - 'array', - '$arrayOfUnknownIntegers + [1, 2, 3]', - ], - [ - '(float|int)', - '$sumWithStaticConst', - ], - [ - '(float|int)', - '$severalSumWithStaticConst1', - ], - [ - '(float|int)', - '$severalSumWithStaticConst2', - ], - [ - '(float|int)', - '$severalSumWithStaticConst3', - ], - [ - '1', - '5 & 3', - ], - [ - 'int', - '$integer & 3', - ], - [ - '\'x\'', - '"x" & "y"', - ], - [ - 'string', - '$string & "x"', - ], - [ - '*ERROR*', - '"bla" & 3', - ], - [ - '1', - '"5" & 3', - ], - [ - '7', - '5 | 3', - ], - [ - 'int', - '$integer | 3', - ], - [ - '\'y\'', - '"x" | "y"', - ], - [ - 'string', - '$string | "x"', - ], - [ - '*ERROR*', - '"bla" | 3', - ], - [ - '7', - '"5" | 3', - ], - [ - '6', - '5 ^ 3', - ], - [ - 'int', - '$integer ^ 3', - ], - [ - '\'' . "\x01" . '\'', - '"x" ^ "y"', - ], - [ - 'string', - '$string ^ "x"', - ], - [ - '*ERROR*', - '"bla" ^ 3', - ], - [ - '6', - '"5" ^ 3', - ], - [ - 'int', - '$integer &= 3', - ], - [ - '*ERROR*', - '$string &= 3', - ], - [ - 'string', - '$string &= "x"', - ], - [ - 'int', - '$integer |= 3', - ], - [ - '*ERROR*', - '$string |= 3', - ], - [ - 'string', - '$string |= "x"', - ], - [ - 'int', - '$integer ^= 3', - ], - [ - '*ERROR*', - '$string ^= 3', - ], - [ - 'string', - '$string ^= "x"', - ], - [ - '\'f\'', - '$fooString[0]', - ], - [ - '*ERROR*', - '$fooString[4]', - ], - [ - 'string', - '$fooString[$integer]', - ], - [ - '\'foo bar\'', - '$foobarString', - ], - [ - '\'foo bar\'', - '"$fooString bar"', - ], - [ - '*ERROR*', - '"$std bar"', - ], - [ - 'array<\'foo\'|int|stdClass>&nonEmpty', - '$arrToPush', - ], - [ - 'array<\'foo\'|int|stdClass>&nonEmpty', - '$arrToPush2', - ], - [ - 'array(0 => \'lorem\', 1 => 5, \'foo\' => stdClass, 2 => \'test\')', - '$arrToUnshift', - ], - [ - 'array<\'lorem\'|int|stdClass>&nonEmpty', - '$arrToUnshift2', - ], - [ - 'array(0 => \'lorem\', 1 => stdClass, 2 => 1, 3 => 1, 4 => 1, ?5 => 2|3, ?6 => 3)', - '$unshiftedConditionalArray', - ], - [ - 'array(\'dirname\' => string, \'basename\' => string, \'filename\' => string, ?\'extension\' => string)', - 'pathinfo($string)', - ], - [ - 'string', - 'pathinfo($string, PATHINFO_DIRNAME)', - ], - [ - 'string', - '$string++', - ], - [ - 'string', - '$string--', - ], - [ - 'string', - '++$string', - ], - [ - 'string', - '--$string', - ], - [ - 'string', - '$incrementedString', - ], - [ - 'string', - '$decrementedString', - ], - [ - '\'foo\'', - '$fooString++', - ], - [ - '\'foo\'', - '$fooString--', - ], - [ - '\'fop\'', - '++$fooString', - ], - [ - '\'foo\'', - '--$fooString', - ], - [ - '\'fop\'', - '$incrementedFooString', - ], - [ - '\'foo\'', - '$decrementedFooString', - ], - [ - 'string', - '$conditionalString . $conditionalString', - ], - [ - 'string', - '$conditionalString . $anotherConditionalString', - ], - [ - 'string', - '$anotherConditionalString . $conditionalString', - ], - [ - '6|7|8', - 'count($conditionalArray) + count($array)', - ], - [ - 'bool', - 'is_numeric($string)', - ], - [ - 'false', - 'is_numeric($fooString)', - ], - [ - 'bool', - 'is_int($mixed)', - ], - [ - 'true', - 'is_int($integer)', - ], - [ - 'false', - 'is_int($string)', - ], - [ - 'bool', - 'in_array(\'foo\', [\'foo\', \'bar\'])', - ], - [ - 'true', - 'in_array(\'foo\', [\'foo\', \'bar\'], true)', - ], - [ - 'false', - 'in_array(\'baz\', [\'foo\', \'bar\'], true)', - ], - [ - 'array(2, 3)', - '$arrToShift', - ], - [ - 'array(1, 2)', - '$arrToPop', - ], - [ - 'class-string', - 'static::class', - ], - [ - '\'NonexistentClass\'', - 'NonexistentClass::class', - ], - [ - 'class-string', - 'parent::class', - ], - [ - 'true', - 'array_key_exists(0, $array)', - ], - [ - 'false', - 'array_key_exists(3, $array)', - ], - [ - 'bool', - 'array_key_exists(3, $conditionalArray)', - ], - [ - 'bool', - 'array_key_exists(\'foo\', $generalArray)', - ], - [ - 'resource', - 'curl_init()', - ], - [ - 'resource|false', - 'curl_init($string)', - ], - [ - 'string', - 'sprintf($string, $string, 1)', - ], - [ - '\'foo bar\'', - "sprintf('%s %s', 'foo', 'bar')", - ], - [ - 'array()|array(0 => \'password\'|\'username\', ?1 => \'password\')', - '$coalesceArray', - ], - [ - 'array', - '$arrayToBeUnset', - ], - [ - 'array', - '$shiftedNonEmptyArray', - ], - [ - 'array&nonEmpty', - '$unshiftedArray', - ], - [ - 'array', - '$poppedNonEmptyArray', - ], - [ - 'array&nonEmpty', - '$pushedArray', - ], - [ - 'string|false', - '$simpleXMLReturningXML', - ], - [ - 'string', - '$xmlString', - ], - [ - 'bool', - '$simpleXMLWritingXML', - ], - ]; - } - - /** - * @dataProvider dataBinaryOperations - * @param string $description - * @param string $expression - */ - public function testBinaryOperations( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/binary.php', - $description, - $expression - ); - } - - public function dataVarStatementAnnotation(): array - { - return [ - [ - 'VarStatementAnnotation\Foo', - '$object', - ], - ]; - } - - /** - * @dataProvider dataVarStatementAnnotation - * @param string $description - * @param string $expression - */ - public function testVarStatementAnnotation( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/var-stmt-annotation.php', - $description, - $expression - ); - } - - public function dataCloneOperators(): array - { - return [ - [ - 'CloneOperators\Foo', - 'clone $fooObject', - ], - ]; - } - - /** - * @dataProvider dataCloneOperators - * @param string $description - * @param string $expression - */ - public function testCloneOperators( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/clone.php', - $description, - $expression - ); - } - - public function dataLiteralArrays(): array - { - return [ - [ - '0', - '$integers[0]', - ], - [ - '1', - '$integers[1]', - ], - [ - '\'foo\'', - '$strings[0]', - ], - [ - '*ERROR*', - '$emptyArray[0]', - ], - [ - '0', - '$mixedArray[0]', - ], - [ - 'true', - '$integers[0] >= $integers[1] - 1', - ], - [ - 'array(\'foo\' => array(\'foo\' => array(\'foo\' => \'bar\')), \'bar\' => array(), \'baz\' => array(\'lorem\' => array()))', - '$nestedArray', - ], - [ - '0', - '$integers[\'0\']', - ], - ]; - } - - /** - * @dataProvider dataLiteralArrays - * @param string $description - * @param string $expression - */ - public function testLiteralArrays( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/literal-arrays.php', - $description, - $expression - ); - } - - public function dataLiteralArraysKeys(): array - { - define('STRING_ONE', '1'); - define('INT_ONE', 1); - define('STRING_FOO', 'foo'); - - return [ - [ - '0|1|2', - "'NoKeysArray'", - ], - [ - '0|1|2', - "'IntegersAndNoKeysArray'", - ], - [ - '0|1|\'foo\'', - "'StringsAndNoKeysArray'", - ], - [ - '1|2|3', - "'IntegersAsStringsAndNoKeysArray'", - ], - [ - '1|2', - "'IntegersAsStringsArray'", - ], - [ - '1|2', - "'IntegersArray'", - ], - [ - '1|2|3', - "'IntegersWithFloatsArray'", - ], - [ - '\'bar\'|\'foo\'', - "'StringsArray'", - ], - [ - '\'\'|\'bar\'|\'baz\'', - "'StringsWithNullArray'", - ], - [ - '1|2|string', - "'IntegersWithStringFromMethodArray'", - ], - [ - '1|2|\'foo\'', - "'IntegersAndStringsArray'", - ], - [ - '0|1', - "'BooleansArray'", - ], - [ - 'int|string', - "'UnknownConstantArray'", - ], - ]; - } - - /** - * @dataProvider dataLiteralArraysKeys - * @param string $description - * @param string $evaluatedPointExpressionType - */ - public function testLiteralArraysKeys( - string $description, - string $evaluatedPointExpressionType - ): void - { - $this->assertTypes( - __DIR__ . '/data/literal-arrays-keys.php', - $description, - '$key', - [], - [], - [], - [], - $evaluatedPointExpressionType - ); - } - - public function dataStringArrayAccess(): array - { - return [ - [ - '*ERROR*', - '$stringFalse', - ], - [ - '*ERROR*', - '$stringObject', - ], - [ - '*ERROR*', - '$stringFloat', - ], - [ - '*ERROR*', - '$stringString', - ], - [ - '*ERROR*', - '$stringArray', - ], - ]; - } - - /** - * @dataProvider dataStringArrayAccess - * @param string $description - * @param string $expression - */ - public function testStringArrayAccess( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/string-array-access.php', - $description, - $expression - ); - } - - public function dataTypeFromFunctionPhpDocs(): array - { - return [ - [ - 'mixed', - '$mixedParameter', - ], - [ - 'MethodPhpDocsNamespace\Bar|MethodPhpDocsNamespace\Foo', - '$unionTypeParameter', - ], - [ - 'int', - '$anotherMixedParameter', - ], - [ - 'mixed', - '$yetAnotherMixedParameter', - ], - [ - 'int', - '$integerParameter', - ], - [ - 'int', - '$anotherIntegerParameter', - ], - [ - 'array', - '$arrayParameterOne', - ], - [ - 'array', - '$arrayParameterOther', - ], - [ - 'MethodPhpDocsNamespace\\Lorem', - '$objectRelative', - ], - [ - 'SomeOtherNamespace\\Ipsum', - '$objectFullyQualified', - ], - [ - 'SomeNamespace\\Amet', - '$objectUsed', - ], - [ - '*ERROR*', - '$nonexistentParameter', - ], - [ - 'int|null', - '$nullableInteger', - ], - [ - 'SomeNamespace\Amet|null', - '$nullableObject', - ], - [ - 'SomeNamespace\Amet|null', - '$anotherNullableObject', - ], - [ - 'null', - '$nullType', - ], - [ - 'MethodPhpDocsNamespace\Bar', - '$barObject->doBar()', - ], - [ - 'MethodPhpDocsNamespace\Bar', - '$conflictedObject', - ], - [ - 'MethodPhpDocsNamespace\Baz', - '$moreSpecifiedObject', - ], - [ - 'MethodPhpDocsNamespace\Baz', - '$moreSpecifiedObject->doFluent()', - ], - [ - 'MethodPhpDocsNamespace\Baz|null', - '$moreSpecifiedObject->doFluentNullable()', - ], - [ - 'MethodPhpDocsNamespace\Baz', - '$moreSpecifiedObject->doFluentArray()[0]', - ], - [ - 'iterable&MethodPhpDocsNamespace\Collection', - '$moreSpecifiedObject->doFluentUnionIterable()', - ], - [ - 'MethodPhpDocsNamespace\Baz', - '$fluentUnionIterableBaz', - ], - [ - 'resource', - '$resource', - ], - [ - 'mixed', - '$yetAnotherAnotherMixedParameter', - ], - [ - 'mixed', - '$yetAnotherAnotherAnotherMixedParameter', - ], - [ - 'void', - '$voidParameter', - ], - [ - 'SomeNamespace\Consecteur', - '$useWithoutAlias', - ], - [ - 'true', - '$true', - ], - [ - 'false', - '$false', - ], - [ - 'true', - '$boolTrue', - ], - [ - 'false', - '$boolFalse', - ], - [ - 'bool', - '$trueBoolean', - ], - [ - 'bool', - '$parameterWithDefaultValueFalse', - ], - ]; - } - - public function dataTypeFromFunctionFunctionPhpDocs(): array - { - return [ - [ - 'MethodPhpDocsNamespace\Foo', - '$fooFunctionResult', - ], - [ - 'MethodPhpDocsNamespace\Bar', - '$barFunctionResult', - ], - ]; - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionFunctionPhpDocs - * @param string $description - * @param string $expression - */ - public function testTypeFromFunctionPhpDocs( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/functionPhpDocs.php'; - $this->assertTypes( - __DIR__ . '/data/functionPhpDocs.php', - $description, - $expression - ); - } - - public function dataTypeFromFunctionPrefixedPhpDocs(): array - { - return [ - [ - 'MethodPhpDocsNamespace\Foo', - '$fooFunctionResult', - ], - ]; - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - * @param string $description - * @param string $expression - */ - public function testTypeFromFunctionPhpDocsPsalmPrefix( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/functionPhpDocs-psalmPrefix.php'; - $this->assertTypes( - __DIR__ . '/data/functionPhpDocs-psalmPrefix.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - * @param string $description - * @param string $expression - */ - public function testTypeFromFunctionPhpDocsPhpstanPrefix( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php'; - $this->assertTypes( - __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php', - $description, - $expression - ); - } - - public function dataTypeFromMethodPhpDocs(): array - { - return [ - [ - 'MethodPhpDocsNamespace\\Foo', - '$selfType', - ], - [ - 'static(MethodPhpDocsNamespace\Foo)', - '$staticType', - false, - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$this->doFoo()', - ], - [ - 'MethodPhpDocsNamespace\Bar', - 'static::doSomethingStatic()', - ], - [ - 'static(MethodPhpDocsNamespace\Foo)', - 'parent::doLorem()', - ], - [ - 'MethodPhpDocsNamespace\FooParent', - '$parent->doLorem()', - false, - ], - [ - 'static(MethodPhpDocsNamespace\Foo)', - '$this->doLorem()', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$differentInstance->doLorem()', - ], - [ - 'static(MethodPhpDocsNamespace\Foo)', - 'parent::doIpsum()', - ], - [ - 'MethodPhpDocsNamespace\FooParent', - '$parent->doIpsum()', - false, - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$differentInstance->doIpsum()', - ], - [ - 'static(MethodPhpDocsNamespace\Foo)', - '$this->doIpsum()', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$this->doBar()[0]', - ], - [ - 'MethodPhpDocsNamespace\Bar', - 'self::doSomethingStatic()', - ], - [ - 'MethodPhpDocsNamespace\Bar', - '\MethodPhpDocsNamespace\Foo::doSomethingStatic()', - ], - [ - '$this(MethodPhpDocsNamespace\Foo)', - 'parent::doThis()', - ], - [ - '$this(MethodPhpDocsNamespace\Foo)|null', - 'parent::doThisNullable()', - ], - [ - '$this(MethodPhpDocsNamespace\Foo)|MethodPhpDocsNamespace\Bar|null', - 'parent::doThisUnion()', - ], - [ - 'MethodPhpDocsNamespace\FooParent', - '$this->returnParent()', - false, - ], - [ - 'MethodPhpDocsNamespace\FooParent', - '$this->returnPhpDocParent()', - false, - ], - [ - 'array', - '$this->returnNulls()', - ], - [ - 'object', - '$objectWithoutNativeTypehint', - ], - [ - 'object', - '$objectWithNativeTypehint', - ], - [ - 'object', - '$this->returnObject()', - ], - [ - 'MethodPhpDocsNamespace\FooParent', - 'new parent()', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$inlineSelf', - ], - [ - 'MethodPhpDocsNamespace\Bar', - '$inlineBar', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$this->phpDocVoidMethod()', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$this->phpDocVoidMethodFromInterface()', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$this->phpDocVoidParentMethod()', - ], - [ - 'MethodPhpDocsNamespace\Foo', - '$this->phpDocWithoutCurlyBracesVoidParentMethod()', - ], - [ - 'array', - '$this->returnsStringArray()', - ], - [ - 'mixed', - '$this->privateMethodWithPhpDoc()', - ], - ]; - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - */ - public function testTypeFromMethodPhpDocs( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ - public function testTypeFromMethodPhpDocsPsalmPrefix( - string $description, - string $expression, - bool $replaceClass = true - ): void - { - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPsalmPrefix)', $description); - - if ($replaceClass && $expression !== '$this->doFoo()') { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPsalmPrefix)', $description); - if ($description === 'MethodPhpDocsNamespace\Foo') { - $description = 'MethodPhpDocsNamespace\FooPsalmPrefix'; - } - } - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs-psalmPrefix.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass = true - */ - public function testTypeFromMethodPhpDocsPhpstanPrefix( - string $description, - string $expression, - bool $replaceClass = true - ): void - { - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhpstanPrefix)', $description); - - if ($replaceClass && $expression !== '$this->doFoo()') { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhpstanPrefix)', $description); - if ($description === 'MethodPhpDocsNamespace\Foo') { - $description = 'MethodPhpDocsNamespace\FooPhpstanPrefix'; - } - } - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs-phpstanPrefix.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ - public function testTypeFromTraitPhpDocs( - string $description, - string $expression, - bool $replaceClass = true - ): void - { - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithTrait)', $description); - - if ($replaceClass && $expression !== '$this->doFoo()') { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooWithTrait)', $description); - if ($description === 'MethodPhpDocsNamespace\Foo') { - $description = 'MethodPhpDocsNamespace\FooWithTrait'; - } - } - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs-trait.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ - public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( - string $description, - string $expression, - bool $replaceClass = true - ): 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('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); - if ($expression === '$inlineSelf') { - $description = 'MethodPhpDocsNamespace\FooInheritDocChild'; - } - } - $this->assertTypes( - __DIR__ . '/data/method-phpDocs-inheritdoc-without-curly-braces.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ - public function testTypeFromRecursiveTraitPhpDocs( - string $description, - string $expression, - bool $replaceClass = true - ): void - { - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithRecursiveTrait)', $description); - - if ($replaceClass && $expression !== '$this->doFoo()') { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooWithRecursiveTrait)', $description); - if ($description === 'MethodPhpDocsNamespace\Foo') { - $description = 'MethodPhpDocsNamespace\FooWithRecursiveTrait'; - } - } - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs-recursiveTrait.php', - $description, - $expression - ); - } - - public function dataTypeFromTraitPhpDocsInSameFile(): array - { - return [ - [ - 'string', - '$this->getFoo()', - ], - ]; - } - - /** - * @dataProvider dataTypeFromTraitPhpDocsInSameFile - * @param string $description - * @param string $expression - */ - public function testTypeFromTraitPhpDocsInSameFile( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs-traitInSameFileAsClass.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ - public function testTypeFromMethodPhpDocsInheritDoc( - string $description, - string $expression, - bool $replaceClass = true - ): 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('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); - if ($expression === '$inlineSelf') { - $description = 'MethodPhpDocsNamespace\FooInheritDocChild'; - } - } - $this->assertTypes( - __DIR__ . '/data/method-phpDocs-inheritdoc.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataTypeFromFunctionPhpDocs - * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass - */ - public function testTypeFromMethodPhpDocsImplicitInheritance( - string $description, - string $expression, - bool $replaceClass = true - ): void - { - if ($replaceClass) { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhpDocsImplicitInheritanceChild)', $description); - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhpDocsImplicitInheritanceChild)', $description); - $description = str_replace('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); - if ($expression === '$inlineSelf') { - $description = 'MethodPhpDocsNamespace\FooPhpDocsImplicitInheritanceChild'; - } - } - $this->assertTypes( - __DIR__ . '/data/methodPhpDocs-implicitInheritance.php', - $description, - $expression - ); - } - - public function testNotSwitchInstanceof(): void - { - $this->assertTypes( - __DIR__ . '/data/switch-instanceof-not.php', - '*ERROR*', - '$foo' - ); - } - - public function dataSwitchInstanceOf(): array - { - return [ - [ - '*ERROR*', - '$foo', - ], - [ - '*ERROR*', - '$bar', - ], - [ - 'SwitchInstanceOf\Baz', - '$baz', - ], - ]; - } - - /** - * @dataProvider dataSwitchInstanceOf - * @param string $description - * @param string $expression - */ - public function testSwitchInstanceof( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/switch-instanceof.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataSwitchInstanceOf - * @param string $description - * @param string $expression - */ - public function testSwitchInstanceofTruthy( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/switch-instanceof-truthy.php', - $description, - $expression - ); - } - - public function dataSwitchGetClass(): array - { - return [ - [ - 'SwitchGetClass\Lorem', - '$lorem', - "'normalName'", - ], - [ - 'SwitchGetClass\Foo', - '$lorem', - "'selfReferentialName'", - ], - ]; - } - - /** - * @dataProvider dataSwitchGetClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testSwitchGetClass( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/switch-get-class.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataSwitchInstanceOfFallthrough(): array - { - return [ - [ - 'SwitchInstanceOfFallthrough\A|SwitchInstanceOfFallthrough\B', - '$object', - ], - ]; - } - - /** - * @dataProvider dataSwitchInstanceOfFallthrough - * @param string $description - * @param string $expression - */ - public function testSwitchInstanceOfFallthrough( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/switch-instanceof-fallthrough.php', - $description, - $expression - ); - } - - public function dataSwitchTypeElimination(): array - { - return [ - [ - 'string', - '$stringOrInt', - ], - ]; - } - - /** - * @dataProvider dataSwitchTypeElimination - * @param string $description - * @param string $expression - */ - public function testSwitchTypeElimination( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/switch-type-elimination.php', - $description, - $expression - ); - } - - public function dataDynamicMethodReturnTypeExtensions(): array - { - return [ - [ - '*ERROR*', - '$em->getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$em->getByPrimary()', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$em->getByPrimary($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '$em->getByPrimary(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - '*ERROR*', - '$iem->getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$iem->getByPrimary()', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$iem->getByPrimary($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '$iem->getByPrimary(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - '*ERROR*', - 'EntityManager::getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity()', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '\DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - '*ERROR*', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity()', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '$container[\DynamicMethodReturnTypesNamespace\Foo::class]', - ], - [ - 'object', - 'new \DynamicMethodReturnTypesNamespace\Foo()', - ], - [ - 'object', - 'new \DynamicMethodReturnTypesNamespace\FooWithoutConstructor()', - ], - ]; - } - - /** - * @dataProvider dataDynamicMethodReturnTypeExtensions - * @param string $description - * @param string $expression - */ - public function testDynamicMethodReturnTypeExtensions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/dynamic-method-return-types.php', - $description, - $expression, - [ - new class() implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\EntityManager::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return in_array($methodReflection->getName(), ['getByPrimary'], true); - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - $args = $methodCall->args; - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $arg = $args[0]->value; - if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - return new ObjectType((string) $arg->class); - } - - }, - new class() implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\ComponentContainer::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === 'offsetGet'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - $args = $methodCall->args; - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $argType = $scope->getType($args[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - return new ObjectType($argType->getValue()); - } - - }, - ], - [ - new class() implements DynamicStaticMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\EntityManager::class; - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return in_array($methodReflection->getName(), ['createManagerForEntity'], true); - } - - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - $args = $methodCall->args; - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $arg = $args[0]->value; - if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - return new ObjectType((string) $arg->class); - } - - }, - new class() implements DynamicStaticMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\Foo::class; - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === '__construct'; - } - - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - return new ObjectWithoutClassType(); - } - - }, - new class() implements DynamicStaticMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\FooWithoutConstructor::class; - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === '__construct'; - } - - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - return new ObjectWithoutClassType(); - } - - }, - ] - ); - } - - public function dataDynamicReturnTypeExtensionsOnCompoundTypes(): array - { - return [ - [ - 'DynamicMethodReturnCompoundTypes\Collection', - '$collection->getSelf()', - ], - [ - 'DynamicMethodReturnCompoundTypes\Collection|DynamicMethodReturnCompoundTypes\Foo', - '$collectionOrFoo->getSelf()', - ], - ]; - } - - /** - * @dataProvider dataDynamicReturnTypeExtensionsOnCompoundTypes - * @param string $description - * @param string $expression - */ - public function testDynamicReturnTypeExtensionsOnCompoundTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/dynamic-method-return-compound-types.php', - $description, - $expression, - [ - new class () implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnCompoundTypes\Collection::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === 'getSelf'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - return new ObjectType(\DynamicMethodReturnCompoundTypes\Collection::class); - } - - }, - new class () implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnCompoundTypes\Foo::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === 'getSelf'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - return new ObjectType(\DynamicMethodReturnCompoundTypes\Foo::class); - } - - }, - ] - ); - } - - public function dataOverwritingVariable(): array - { - return [ - [ - 'mixed', - '$var', - 'new \OverwritingVariable\Bar()', - ], - [ - 'OverwritingVariable\Bar', - '$var', - '$var->methodFoo()', - ], - [ - 'OverwritingVariable\Foo', - '$var', - 'die', - ], - ]; - } - - /** - * @dataProvider dataOverwritingVariable - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpressionType - */ - public function testOverwritingVariable( - string $description, - string $expression, - string $evaluatedPointExpressionType - ): void - { - $this->assertTypes( - __DIR__ . '/data/overwritingVariable.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpressionType - ); - } - - public function dataNegatedInstanceof(): array - { - return [ - [ - 'NegatedInstanceOf\Foo', - '$foo', - ], - [ - 'NegatedInstanceOf\Bar', - '$bar', - ], - [ - 'mixed', - '$lorem', - ], - [ - 'mixed~NegatedInstanceOf\Dolor', - '$dolor', - ], - [ - 'mixed~NegatedInstanceOf\Sit', - '$sit', - ], - [ - 'mixed', - '$mixedFoo', - ], - [ - 'mixed', - '$mixedBar', - ], - [ - 'NegatedInstanceOf\Foo', - '$self', - ], - [ - 'static(NegatedInstanceOf\Foo)', - '$static', - ], - [ - 'NegatedInstanceOf\Foo', - '$anotherFoo', - ], - [ - 'NegatedInstanceOf\Bar&NegatedInstanceOf\Foo', - '$fooAndBar', - ], - ]; - } - - /** - * @dataProvider dataNegatedInstanceof - * @param string $description - * @param string $expression - */ - public function testNegatedInstanceof( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/negated-instanceof.php', - $description, - $expression - ); - } - - public function dataAnonymousFunction(): array - { - return [ - [ - 'string', - '$str', - ], - [ - '1', - '$integer', - ], - [ - '*ERROR*', - '$bar', - ], - ]; - } - - /** - * @dataProvider dataAnonymousFunction - * @param string $description - * @param string $expression - */ - public function testAnonymousFunction( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/anonymous-function.php', - $description, - $expression - ); - } - - public function dataForeachArrayType(): array - { - return [ - [ - __DIR__ . '/data/foreach/array-object-type.php', - 'AnotherNamespace\Foo', - '$foo', - ], - [ - __DIR__ . '/data/foreach/array-object-type.php', - 'AnotherNamespace\Foo', - '$foos[0]', - ], - [ - __DIR__ . '/data/foreach/array-object-type.php', - '0', - 'self::ARRAY_CONSTANT[0]', - ], - [ - __DIR__ . '/data/foreach/array-object-type.php', - '\'foo\'', - 'self::MIXED_CONSTANT[1]', - ], - [ - __DIR__ . '/data/foreach/nested-object-type.php', - 'AnotherNamespace\Foo', - '$foo', - ], - [ - __DIR__ . '/data/foreach/nested-object-type.php', - 'AnotherNamespace\Foo', - '$foos[0]', - ], - [ - __DIR__ . '/data/foreach/nested-object-type.php', - 'AnotherNamespace\Foo', - '$fooses[0][0]', - ], - [ - __DIR__ . '/data/foreach/integer-type.php', - 'int', - '$integer', - ], - [ - __DIR__ . '/data/foreach/reusing-specified-variable.php', - '1|2|3', - '$business', - ], - [ - __DIR__ . '/data/foreach/type-in-comment-variable-first.php', - 'mixed', - '$value', - ], - [ - __DIR__ . '/data/foreach/type-in-comment-variable-second.php', - 'stdClass', - '$value', - ], - [ - __DIR__ . '/data/foreach/type-in-comment-no-variable.php', - 'mixed', - '$value', - ], - [ - __DIR__ . '/data/foreach/type-in-comment-wrong-variable.php', - 'mixed', - '$value', - ], - [ - __DIR__ . '/data/foreach/type-in-comment-variable-with-reference.php', - 'string', - '$value', - ], - [ - __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', - 'array', - '$list', - ], - [ - __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', - 'string', - '$key', - ], - [ - __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', - 'float|int|string', - '$value', - ], - [ - __DIR__ . '/data/foreach/foreach-with-complex-value-type.php', - 'float|ForeachWithComplexValueType\Foo', - '$value', - ], - [ - __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', - 'ForeachWithGenericsPhpDoc\Bar|ForeachWithGenericsPhpDoc\Foo', - '$key', - ], - [ - __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', - 'float|int|string', - '$value', - ], - [ - __DIR__ . '/data/foreach/foreach-iterable-with-complex-value-type.php', - 'float|ForeachWithComplexValueType\Foo', - '$value', - ], - [ - __DIR__ . '/data/foreach/type-in-comment-key.php', - 'int', - '$key', - ], - ]; - } - - /** - * @dataProvider dataForeachArrayType - * @param string $file - * @param string $description - * @param string $expression - */ - public function testForeachArrayType( - string $file, - string $description, - string $expression - ): void - { - $this->assertTypes( - $file, - $description, - $expression - ); - } - - public function dataOverridingSpecifiedType(): array - { - return [ - [ - __DIR__ . '/data/catch-specified-variable.php', - 'TryCatchWithSpecifiedVariable\FooException', - '$foo', - ], - ]; - } - - /** - * @dataProvider dataOverridingSpecifiedType - * @param string $file - * @param string $description - * @param string $expression - */ - public function testOverridingSpecifiedType( - string $file, - string $description, - string $expression - ): void - { - $this->assertTypes( - $file, - $description, - $expression - ); - } - - public function dataForeachObjectType(): array - { - return [ - [ - __DIR__ . '/data/foreach/object-type.php', - 'ObjectType\\MyKey', - '$keyFromIterator', - "'insideFirstForeach'", - ], - [ - __DIR__ . '/data/foreach/object-type.php', - 'ObjectType\\MyValue', - '$valueFromIterator', - "'insideFirstForeach'", - ], - [ - __DIR__ . '/data/foreach/object-type.php', - 'ObjectType\\MyKey', - '$keyFromAggregate', - "'insideSecondForeach'", - ], - [ - __DIR__ . '/data/foreach/object-type.php', - 'ObjectType\\MyValue', - '$valueFromAggregate', - "'insideSecondForeach'", - ], - [ - __DIR__ . '/data/foreach/object-type.php', - '*ERROR*', - '$keyFromRecursiveAggregate', - "'insideThirdForeach'", - ], - [ - __DIR__ . '/data/foreach/object-type.php', - '*ERROR*', - '$valueFromRecursiveAggregate', - "'insideThirdForeach'", - ], - ]; - } - - /** - * @dataProvider dataForeachObjectType - * @param string $file - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testForeachObjectType( - string $file, - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - $file, - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataArrayFunctions(): array - { - return [ - [ - '1', - '$integers[0]', - ], - [ - 'array(string, string, string)', - '$mappedStrings', - ], - [ - 'string', - '$mappedStrings[0]', - ], - [ - '1|2|3', - '$filteredIntegers[0]', - ], - [ - '123', - '$filteredMixed[0]', - ], - [ - '1|2|3', - '$uniquedIntegers[1]', - ], - [ - 'string', - '$reducedIntegersToString', - ], - [ - 'string|null', - '$reducedIntegersToStringWithNull', - ], - [ - 'string', - '$reducedIntegersToStringAnother', - ], - [ - 'null', - '$reducedToNull', - ], - [ - '1|string', - '$reducedIntegersToStringWithInt', - ], - [ - '1', - '$reducedToInt', - ], - [ - '1|2|3', - '$reversedIntegers[0]', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_change_key_case($integers)', - ], - [ - 'array|false', - 'array_combine([1], [2])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_diff_assoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_diff_key($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_diff_uassoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_diff_ukey($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_diff($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_udiff_assoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_udiff_uassoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_udiff($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_intersect_assoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_intersect_key($integers, [])', - ], - [ - 'array', - 'array_intersect_key(...[$integers, [4, 5, 6]])', - ], - [ - 'array', - 'array_intersect_key(...$generalIntegersInAnotherArray, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_intersect_uassoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_intersect_ukey($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_intersect($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_uintersect_assoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_uintersect_uassoc($integers, [])', - ], - [ - 'array<0|1|2, 1|2|3>', - 'array_uintersect($integers, [])', - ], - [ - 'array(1, 1, 1, 1, 1)', - '$filledIntegers', - ], - [ - 'array(1)', - '$filledIntegersWithKeys', - ], - [ - 'array(1, 2)', - 'array_keys($integerKeys)', - ], - [ - 'array(\'foo\', \'bar\')', - 'array_keys($stringKeys)', - ], - [ - 'array(\'foo\', 1)', - 'array_keys($stringOrIntegerKeys)', - ], - [ - 'array', - 'array_keys($generalStringKeys)', - ], - [ - 'array(\'foo\', stdClass)', - 'array_values($integerKeys)', - ], - [ - 'array', - 'array_values($generalStringKeys)', - ], - [ - 'array', - 'array_merge($stringOrIntegerKeys)', - ], - [ - 'array', - 'array_merge($generalStringKeys, $generalDateTimeValues)', - ], - [ - 'array', - 'array_merge($generalStringKeys, $stringOrIntegerKeys)', - ], - [ - 'array', - 'array_merge($stringOrIntegerKeys, $generalStringKeys)', - ], - [ - 'array', - 'array_merge($stringKeys, $stringOrIntegerKeys)', - ], - [ - 'array', - 'array_merge($stringOrIntegerKeys, $stringKeys)', - ], - [ - 'array', - 'array_merge(array("color" => "red", 2, 4), array("a", "b", "color" => "green", "shape" => "trapezoid", 4))', - ], - [ - 'array', - 'array_merge(...[$generalStringKeys, $generalDateTimeValues])', - ], - [ - 'array', - '$mergedInts', - ], - [ - 'array(5 => \'banana\', 6 => \'banana\', 7 => \'banana\', 8 => \'banana\', 9 => \'banana\', 10 => \'banana\')', - 'array_fill(5, 6, \'banana\')', - ], - [ - 'array&nonEmpty', - 'array_fill(0, 101, \'apple\')', - ], - [ - 'array(-2 => \'pear\', 0 => \'pear\', 1 => \'pear\', 2 => \'pear\')', - 'array_fill(-2, 4, \'pear\')', - ], - [ - 'array&nonEmpty', - 'array_fill($integer, 2, new \stdClass())', - ], - [ - 'array', - 'array_fill(2, $integer, new \stdClass())', - ], - [ - 'array', - 'array_fill_keys($generalStringKeys, new \stdClass())', - ], - [ - 'array(\'foo\' => \'banana\', 5 => \'banana\', 10 => \'banana\', \'bar\' => \'banana\')', - 'array_fill_keys([\'foo\', 5, 10, \'bar\'], \'banana\')', - ], - [ - 'array', - '$mappedStringKeys', - ], - [ - 'array', - '$mappedStringKeysWithUnknownClosureType', - ], - [ - 'array', - '$mappedWrongArray', - ], - [ - 'array', - '$unknownArray', - ], - [ - 'array(\'foo\' => \'banana\', \'bar\' => \'banana\', ?\'baz\' => \'banana\', ?\'lorem\' => \'banana\')', - 'array_fill_keys($conditionalArray, \'banana\')', - ], - [ - 'array(\'foo\' => stdClass, \'bar\' => stdClass, ?\'baz\' => stdClass, ?\'lorem\' => stdClass)', - 'array_map(function (): \stdClass {}, $conditionalKeysArray)', - ], - [ - 'stdClass', - 'array_pop($stringKeys)', - ], - [ - 'array&hasOffset(\'baz\')', - '$stdClassesWithIsset', - ], - [ - 'stdClass', - 'array_pop($stdClassesWithIsset)', - ], - [ - '\'foo\'', - 'array_shift($stringKeys)', - ], - [ - 'int|null', - 'array_pop($generalStringKeys)', - ], - [ - 'int|null', - 'array_shift($generalStringKeys)', - ], - [ - 'null', - 'array_pop([])', - ], - [ - 'null', - 'array_shift([])', - ], - [ - 'array(null, \'\', 1)', - '$constantArrayWithFalseyValues', - ], - [ - 'array(2 => 1)', - '$constantTruthyValues', - ], - [ - 'array', - '$falsey', - ], - [ - 'array()', - 'array_filter($falsey)', - ], - [ - 'array', - '$withFalsey', - ], - [ - 'array', - 'array_filter($withFalsey)', - ], - [ - 'array(\'a\' => 1)', - 'array_filter($union)', - ], - [ - 'array|int|true>', - 'array_filter($withPossiblyFalsey)', - ], - [ - '(array|null)', - 'array_filter($mixed)', - ], - [ - '1|\'foo\'|false', - 'array_search(new stdClass, $stringOrIntegerKeys, true)', - ], - [ - '\'foo\'', - 'array_search(\'foo\', $stringKeys, true)', - ], - [ - 'int|false', - 'array_search(new DateTimeImmutable(), $generalDateTimeValues, true)', - ], - [ - 'string|false', - 'array_search(9, $generalStringKeys, true)', - ], - [ - 'string|false', - 'array_search(9, $generalStringKeys, false)', - ], - [ - 'string|false', - 'array_search(9, $generalStringKeys)', - ], - [ - 'null', - 'array_search(999, $integer, true)', - ], - [ - 'false', - 'array_search(new stdClass, $generalStringKeys, true)', - ], - [ - 'int|string|false', - 'array_search($mixed, $array, true)', - ], - [ - 'int|string|false', - 'array_search($mixed, $array, false)', - ], - [ - '\'a\'|\'b\'|false', - 'array_search($string, [\'a\' => \'A\', \'b\' => \'B\'], true)', - ], - [ - 'false', - 'array_search($integer, [\'a\' => \'A\', \'b\' => \'B\'], true)', - ], - [ - '\'foo\'|false', - 'array_search($generalIntegerOrString, $stringKeys, true)', - ], - [ - 'int|false', - 'array_search($generalIntegerOrString, $generalArrayOfIntegersOrStrings, true)', - ], - [ - 'int|false', - 'array_search($generalIntegerOrString, $clonedConditionalArray, true)', - ], - [ - 'int|string|false', - 'array_search($generalIntegerOrString, $generalIntegerOrStringKeys, false)', - ], - [ - 'false', - 'array_search(\'id\', $generalIntegerOrStringKeys, true)', - ], - [ - 'int|string|false', - 'array_search(\'id\', $generalIntegerOrStringKeysMixedValues, true)', - ], - [ - 'int|string|false|null', - 'array_search(\'id\', doFoo() ? $generalIntegerOrStringKeys : false, true)', - ], - [ - 'false|null', - 'array_search(\'id\', doFoo() ? [] : false, true)', - ], - [ - 'null', - 'array_search(\'id\', false, true)', - ], - [ - 'null', - 'array_search(\'id\', false)', - ], - [ - 'int|string|false', - 'array_search(\'id\', $thisDoesNotExistAndIsMixed, true)', - ], - [ - 'int|string|false', - 'array_search(\'id\', doFoo() ? $thisDoesNotExistAndIsMixedInUnion : false, true)', - ], - [ - 'int|string|false', - 'array_search(1, $generalIntegers, true)', - ], - [ - 'int|string|false', - 'array_search(1, $generalIntegers, false)', - ], - [ - 'int|string|false', - 'array_search(1, $generalIntegers)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 0)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 1)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 1, null, true)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 1, 2)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 1, 2, true)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 1, -1)', - ], - [ - 'array', - 'array_slice($generalStringKeys, 1, -1, true)', - ], - [ - 'array', - 'array_slice($generalStringKeys, -2)', - ], - [ - 'array', - 'array_slice($generalStringKeys, -2, 1, true)', - ], - [ - 'array', - 'array_slice($unknownArray, 0)', - ], - [ - 'array', - 'array_slice($unknownArray, 1)', - ], - [ - 'array', - 'array_slice($unknownArray, 1, null, true)', - ], - [ - 'array', - 'array_slice($unknownArray, 1, 2)', - ], - [ - 'array', - 'array_slice($unknownArray, 1, 2, true)', - ], - [ - 'array', - 'array_slice($unknownArray, 1, -1)', - ], - [ - 'array', - 'array_slice($unknownArray, 1, -1, true)', - ], - [ - 'array', - 'array_slice($unknownArray, -2)', - ], - [ - 'array', - 'array_slice($unknownArray, -2, 1, true)', - ], - [ - 'array(0 => bool, 1 => int, 2 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, 0)', - ], - [ - 'array(0 => int, 1 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, 1)', - ], - [ - 'array(1 => int, 2 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, 1, null, true)', - ], - [ - 'array(0 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, 2, 3)', - ], - [ - 'array(2 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, 2, 3, true)', - ], - [ - 'array(int, \'\')', - 'array_slice($withPossiblyFalsey, 1, -1)', - ], - [ - 'array(1 => int, 2 => \'\')', - 'array_slice($withPossiblyFalsey, 1, -1, true)', - ], - [ - 'array(0 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, -2, null)', - ], - [ - 'array(2 => \'\', \'a\' => 0)', - 'array_slice($withPossiblyFalsey, -2, null, true)', - ], - [ - 'array(\'baz\' => \'qux\')|array(0 => \'\', \'a\' => 0)', - 'array_slice($unionArrays, 1)', - ], - [ - 'array(\'a\' => 0)|array(\'baz\' => \'qux\')', - 'array_slice($unionArrays, -1, null, true)', - ], - [ - 'array(0 => \'foo\', 1 => \'bar\', \'baz\' => \'qux\', 2 => \'quux\', \'quuz\' => \'corge\', 3 => \'grault\')', - '$slicedOffset', - ], - [ - 'array(4 => \'foo\', 1 => \'bar\', \'baz\' => \'qux\', 0 => \'quux\', \'quuz\' => \'corge\', 5 => \'grault\')', - '$slicedOffsetWithKeys', - ], - [ - '0|1', - 'key($mixedValues)', - ], - [ - 'int|null', - 'key($falsey)', - ], - [ - 'string|null', - 'key($generalStringKeys)', - ], - [ - 'int|string|null', - 'key($generalIntegerOrStringKeysMixedValues)', - ], - ]; - } - - /** - * @dataProvider dataArrayFunctions - * @param string $description - * @param string $expression - */ - public function testArrayFunctions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/array-functions.php', - $description, - $expression - ); - } - - public function dataFunctions(): array - { - return [ - [ - 'string', - '$microtimeStringWithoutArg', - ], - [ - 'string', - '$microtimeString', - ], - [ - 'float', - '$microtimeFloat', - ], - [ - 'float|string', - '$microtimeDefault', - ], - [ - '(float|string)', - '$microtimeBenevolent', - ], - [ - 'int', - '$strtotimeNow', - ], - [ - 'false', - '$strtotimeInvalid', - ], - [ - 'int|false', - '$strtotimeUnknown', - ], - [ - '(int|false)', - '$strtotimeUnknown2', - ], - [ - 'int|false', - '$strtotimeCrash', - ], - [ - '-1', - '$versionCompare1', - ], - [ - '-1|1', - '$versionCompare2', - ], - [ - '-1|0|1', - '$versionCompare3', - ], - [ - '-1|0|1', - '$versionCompare4', - ], - [ - 'true', - '$versionCompare5', - ], - [ - 'bool', - '$versionCompare6', - ], - [ - 'bool', - '$versionCompare7', - ], - [ - 'bool', - '$versionCompare8', - ], - [ - 'int', - '$mbStrlenWithoutEncoding', - ], - [ - 'int', - '$mbStrlenWithValidEncoding', - ], - [ - 'int', - '$mbStrlenWithValidEncodingAlias', - ], - [ - 'false', - '$mbStrlenWithInvalidEncoding', - ], - [ - 'int|false', - '$mbStrlenWithValidAndInvalidEncoding', - ], - [ - 'int|false', - '$mbStrlenWithUnknownEncoding', - ], - [ - 'string', - '$mbHttpOutputWithoutEncoding', - ], - [ - 'true', - '$mbHttpOutputWithValidEncoding', - ], - [ - 'false', - '$mbHttpOutputWithInvalidEncoding', - ], - [ - 'bool', - '$mbHttpOutputWithValidAndInvalidEncoding', - ], - [ - 'bool', - '$mbHttpOutputWithUnknownEncoding', - ], - [ - 'string', - '$mbRegexEncodingWithoutEncoding', - ], - [ - 'true', - '$mbRegexEncodingWithValidEncoding', - ], - [ - 'false', - '$mbRegexEncodingWithInvalidEncoding', - ], - [ - 'bool', - '$mbRegexEncodingWithValidAndInvalidEncoding', - ], - [ - 'bool', - '$mbRegexEncodingWithUnknownEncoding', - ], - [ - 'string', - '$mbInternalEncodingWithoutEncoding', - ], - [ - 'true', - '$mbInternalEncodingWithValidEncoding', - ], - [ - 'false', - '$mbInternalEncodingWithInvalidEncoding', - ], - [ - 'bool', - '$mbInternalEncodingWithValidAndInvalidEncoding', - ], - [ - 'bool', - '$mbInternalEncodingWithUnknownEncoding', - ], - [ - 'array', - '$mbEncodingAliasesWithValidEncoding', - ], - [ - 'false', - '$mbEncodingAliasesWithInvalidEncoding', - ], - [ - 'array|false', - '$mbEncodingAliasesWithValidAndInvalidEncoding', - ], - [ - 'array|false', - '$mbEncodingAliasesWithUnknownEncoding', - ], - [ - 'string', - '$mbChrWithoutEncoding', - ], - [ - 'string', - '$mbChrWithValidEncoding', - ], - [ - 'false', - '$mbChrWithInvalidEncoding', - ], - [ - 'string|false', - '$mbChrWithValidAndInvalidEncoding', - ], - [ - 'string|false', - '$mbChrWithUnknownEncoding', - ], - [ - 'int', - '$mbOrdWithoutEncoding', - ], - [ - 'int', - '$mbOrdWithValidEncoding', - ], - [ - 'false', - '$mbOrdWithInvalidEncoding', - ], - [ - 'int|false', - '$mbOrdWithValidAndInvalidEncoding', - ], - [ - 'int|false', - '$mbOrdWithUnknownEncoding', - ], - [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)', - '$gettimeofdayArrayWithoutArg', - ], - [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)', - '$gettimeofdayArray', - ], - [ - 'float', - '$gettimeofdayFloat', - ], - [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)|float', - '$gettimeofdayDefault', - ], - [ - '(array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)|float)', - '$gettimeofdayBenevolent', - ], - [ - 'array|false', - '$strSplitConstantStringWithoutDefinedParameters', - ], - [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$strSplitConstantStringWithoutDefinedSplitLength', - ], - [ - 'array', - '$strSplitStringWithoutDefinedSplitLength', - ], - [ - "array('a', 'b', 'c', 'd', 'e', 'f')", - '$strSplitConstantStringWithOneSplitLength', - ], - [ - "array('abcdef')", - '$strSplitConstantStringWithGreaterSplitLengthThanStringLength', - ], - [ - 'false', - '$strSplitConstantStringWithFailureSplitLength', - ], - [ - 'array|false', - '$strSplitConstantStringWithInvalidSplitLengthType', - ], - [ - 'array', - '$strSplitConstantStringWithVariableStringAndConstantSplitLength', - ], - [ - 'array|false', - '$strSplitConstantStringWithVariableStringAndVariableSplitLength', - ], - // parse_url - [ - 'mixed', - '$parseUrlWithoutParameters', - ], - [ - "array('scheme' => 'http', 'host' => 'abc.def')", - '$parseUrlConstantUrlWithoutComponent1', - ], - [ - "array('scheme' => 'http', 'host' => 'def.abc')", - '$parseUrlConstantUrlWithoutComponent2', - ], - [ - "array(?'scheme' => string, ?'host' => string, ?'port' => int, ?'user' => string, ?'pass' => string, ?'path' => string, ?'query' => string, ?'fragment' => string)|false", - '$parseUrlConstantUrlUnknownComponent', - ], - [ - 'null', - '$parseUrlConstantUrlWithComponentNull', - ], - [ - "'this-is-fragment'", - '$parseUrlConstantUrlWithComponentSet', - ], - [ - 'false', - '$parseUrlConstantUrlWithComponentInvalid', - ], - [ - 'false', - '$parseUrlStringUrlWithComponentInvalid', - ], - [ - 'int|false|null', - '$parseUrlStringUrlWithComponentPort', - ], - [ - "array(?'scheme' => string, ?'host' => string, ?'port' => int, ?'user' => string, ?'pass' => string, ?'path' => string, ?'query' => string, ?'fragment' => string)|false", - '$parseUrlStringUrlWithoutComponent', - ], - [ - "array('path' => 'abc.def')", - "parse_url('http://github.com/abc.def')", - ], - [ - 'null', - "parse_url('http://github.com/abc.def',%20PHP_URL_SCHEME)", - ], - [ - "'http'", - "parse_url('http://abc.def/', PHP_URL_SCHEME)", - ], - [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', - '$stat', - ], - [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', - '$lstat', - ], - [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', - '$fstat', - ], - [ - 'string', - '$base64DecodeWithoutStrict', - ], - [ - 'string', - '$base64DecodeWithStrictDisabled', - ], - [ - 'string|false', - '$base64DecodeWithStrictEnabled', - ], - [ - 'string', - '$base64DecodeDefault', - ], - [ - '(string|false)', - '$base64DecodeBenevolent', - ], - [ - '*ERROR*', - '$strWordCountWithoutParameters', - ], - [ - '*ERROR*', - '$strWordCountWithTooManyParams', - ], - [ - 'int', - '$strWordCountStr', - ], - [ - 'int', - '$strWordCountStrType0', - ], - [ - 'array', - '$strWordCountStrType1', - ], - [ - 'array', - '$strWordCountStrType1Extra', - ], - [ - 'array', - '$strWordCountStrType2', - ], - [ - 'array', - '$strWordCountStrType2Extra', - ], - [ - 'array|int|false', - '$strWordCountStrTypeIndeterminant', - ], - ]; - } - - /** - * @dataProvider dataFunctions - * @param string $description - * @param string $expression - */ - public function testFunctions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/functions.php', - $description, - $expression - ); - } - - public function dataDioFunctions(): array - { - return [ - [ - 'array(\'device\' => int, \'inode\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'device_type\' => int, \'size\' => int, \'blocksize\' => int, \'blocks\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int)|null', - '$stat', - ], - ]; - } - - /** - * @dataProvider dataDioFunctions - * @param string $description - * @param string $expression - */ - public function testDioFunctions( - string $description, - string $expression - ): void - { - if (!function_exists('dio_stat')) { - $this->markTestSkipped('This test requires DIO extension.'); - } - $this->assertTypes( - __DIR__ . '/data/dio-functions.php', - $description, - $expression - ); - } - - public function dataSsh2Functions(): array - { - return [ - [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', - '$ssh2SftpStat', - ], - ]; - } - - /** - * @dataProvider dataSsh2Functions - * @param string $description - * @param string $expression - */ - public function testSsh2Functions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/ssh2-functions.php', - $description, - $expression - ); - } - - public function dataRangeFunction(): array - { - return [ - [ - 'array(2, 3, 4, 5)', - 'range(2, 5)', - ], - [ - 'array(2, 4)', - 'range(2, 5, 2)', - ], - [ - 'array(2.0, 3.0, 4.0, 5.0)', - 'range(2, 5, 1.0)', - ], - [ - 'array(2.1, 3.1, 4.1)', - 'range(2.1, 5)', - ], - [ - 'array', - 'range(2, 5, $integer)', - ], - [ - 'array', - 'range($float, 5, $integer)', - ], - [ - 'array', - 'range($float, $mixed, $integer)', - ], - [ - 'array', - 'range($integer, $mixed)', - ], - [ - 'array(0 => 1, ?1 => 2)', - 'range(1, doFoo() ? 1 : 2)', - ], - [ - 'array(0 => -1|1, ?1 => 0|2, ?2 => 1, ?3 => 2)', - 'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)', - ], - [ - 'array(3, 2, 1, 0, -1)', - 'range(3, -1)', - ], - [ - 'array', - 'range(0, 50)', - ], - ]; - } - - /** - * @dataProvider dataRangeFunction - * @param string $description - * @param string $expression - */ - public function testRangeFunction( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/range-function.php', - $description, - $expression - ); - } - - public function dataSpecifiedTypesUsingIsFunctions(): array - { - return [ - [ - 'int', - '$integer', - ], - [ - 'int', - '$anotherInteger', - ], - [ - 'int', - '$longInteger', - ], - [ - 'float', - '$float', - ], - [ - 'float', - '$doubleFloat', - ], - [ - 'float', - '$realFloat', - ], - [ - 'null', - '$null', - ], - [ - 'array', - '$array', - ], - [ - 'bool', - '$bool', - ], - [ - 'callable(): mixed', - '$callable', - ], - [ - 'resource', - '$resource', - ], - [ - 'int', - '$yetAnotherInteger', - ], - [ - '*ERROR*', - '$mixedInteger', - ], - [ - 'string', - '$string', - ], - [ - 'object', - '$object', - ], - [ - 'int', - '$intOrStdClass', - ], - [ - 'Foo', - '$foo', - ], - [ - 'Foo', - '$anotherFoo', - ], - [ - 'class-string|Foo', - '$subClassOfFoo', - ], - [ - 'Foo', - '$subClassOfFoo2', - ], - [ - 'class-string|object', - '$subClassOfFoo3', - ], - [ - 'object', - '$subClassOfFoo4', - ], - [ - 'class-string|Foo', - '$subClassOfFoo5', - ], - [ - 'class-string|object', - '$subClassOfFoo6', - ], - [ - 'Foo', - '$subClassOfFoo7', - ], - [ - 'object', - '$subClassOfFoo8', - ], - [ - 'object', - '$subClassOfFoo9', - ], - [ - 'object', - '$subClassOfFoo10', - ], - [ - 'Foo', - '$subClassOfFoo11', - ], - [ - 'Foo', - '$subClassOfFoo12', - ], - [ - 'Foo', - '$subClassOfFoo13', - ], - [ - 'object', - '$subClassOfFoo14', - ], - [ - 'class-string|Foo', - '$subClassOfFoo15', - ], - [ - 'Bar|class-string|Foo', - '$subClassOfFoo16', - ], - ]; - } - - /** - * @dataProvider dataSpecifiedTypesUsingIsFunctions - * @param string $description - * @param string $expression - */ - public function testSpecifiedTypesUsingIsFunctions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/specifiedTypesUsingIsFunctions.php', - $description, - $expression - ); - } - - public function dataTypeSpecifyingExtensions(): array - { - return [ - [ - 'string', - '$foo', - true, - ], - [ - 'int', - '$bar', - true, - ], - [ - 'string|null', - '$foo', - false, - ], - [ - 'int|null', - '$bar', - false, - ], - [ - 'string', - '$foo', - null, - ], - [ - 'int', - '$bar', - null, - ], - ]; - } - - /** - * @dataProvider dataTypeSpecifyingExtensions - * @param string $description - * @param string $expression - * @param bool|null $nullContext - */ - public function testTypeSpecifyingExtensions( - string $description, - string $expression, - ?bool $nullContext - ): void - { - $this->assertTypes( - __DIR__ . '/data/type-specifying-extensions.php', - $description, - $expression, - [], - [], - [new AssertionClassMethodTypeSpecifyingExtension($nullContext)], - [new AssertionClassStaticMethodTypeSpecifyingExtension($nullContext)], - 'die', - [], - false - ); - } - - public function dataTypeSpecifyingExtensions2(): array - { - return [ - [ - 'string|null', - '$foo', - true, - ], - [ - 'int|null', - '$bar', - true, - ], - [ - 'string|null', - '$foo', - false, - ], - [ - 'int|null', - '$bar', - false, - ], - [ - 'string|null', - '$foo', - null, - ], - [ - 'int|null', - '$bar', - null, - ], - ]; - } - - /** - * @dataProvider dataTypeSpecifyingExtensions2 - * @param string $description - * @param string $expression - * @param bool|null $nullContext - */ - public function testTypeSpecifyingExtensions2( - string $description, - string $expression, - ?bool $nullContext - ): void - { - $this->assertTypes( - __DIR__ . '/data/type-specifying-extensions2.php', - $description, - $expression, - [], - [], - [new AssertionClassMethodTypeSpecifyingExtension($nullContext)], - [new AssertionClassStaticMethodTypeSpecifyingExtension($nullContext)] - ); - } - - public function dataTypeSpecifyingExtensions3(): array - { - return [ - [ - 'string', - '$foo', - false, - ], - [ - 'int', - '$bar', - false, - ], - [ - 'string|null', - '$foo', - true, - ], - [ - 'int|null', - '$bar', - true, - ], - [ - 'string', - '$foo', - null, - ], - [ - 'int', - '$bar', - null, - ], - ]; - } - - /** - * @dataProvider dataTypeSpecifyingExtensions3 - * @param string $description - * @param string $expression - * @param bool|null $nullContext - */ - public function testTypeSpecifyingExtensions3( - string $description, - string $expression, - ?bool $nullContext - ): void - { - $this->assertTypes( - __DIR__ . '/data/type-specifying-extensions3.php', - $description, - $expression, - [], - [], - [new AssertionClassMethodTypeSpecifyingExtension($nullContext)], - [new AssertionClassStaticMethodTypeSpecifyingExtension($nullContext)], - 'die', - [], - false - ); - } - - public function dataIterable(): array - { - return [ - [ - 'iterable', - '$this->iterableProperty', - ], - [ - 'iterable', - '$iterableSpecifiedLater', - ], - [ - 'iterable', - '$iterableWithoutTypehint', - ], - [ - 'mixed', - '$iterableWithoutTypehint[0]', - ], - [ - 'iterable', - '$iterableWithIterableTypehint', - ], - [ - 'mixed', - '$iterableWithIterableTypehint[0]', - ], - [ - 'mixed', - '$mixed', - ], - [ - 'iterable', - '$iterableWithConcreteTypehint', - ], - [ - 'mixed', - '$iterableWithConcreteTypehint[0]', - ], - [ - 'Iterables\Bar', - '$bar', - ], - [ - 'iterable', - '$this->doBar()', - ], - [ - 'iterable', - '$this->doBaz()', - ], - [ - 'Iterables\Baz', - '$baz', - ], - [ - 'array', - '$arrayWithIterableTypehint', - ], - [ - 'mixed', - '$arrayWithIterableTypehint[0]', - ], - [ - 'iterable&Iterables\Collection', - '$unionIterableType', - ], - [ - 'Iterables\Bar', - '$unionBar', - ], - [ - 'array', - '$mixedUnionIterableType', - ], - [ - 'iterable&Iterables\Collection', - '$unionIterableIterableType', - ], - [ - 'mixed', - '$mixedBar', - ], - [ - 'Iterables\Bar', - '$iterableUnionBar', - ], - [ - 'Iterables\Bar', - '$unionBarFromMethod', - ], - [ - 'iterable', - '$this->stringIterableProperty', - ], - [ - 'iterable', - '$this->mixedIterableProperty', - ], - [ - 'iterable', - '$integers', - ], - [ - 'iterable', - '$mixeds', - ], - [ - 'iterable', - '$this->returnIterableMixed()', - ], - [ - 'iterable', - '$this->returnIterableString()', - ], - [ - 'int|iterable', - '$this->iterablePropertyAlsoWithSomethingElse', - ], - [ - 'int|iterable', - '$this->iterablePropertyWithTwoItemTypes', - ], - [ - 'array|Iterables\CollectionOfIntegers', - '$this->collectionOfIntegersOrArrayOfStrings', - ], - [ - 'Generator', - '$generatorOfFoos', - ], - [ - 'Iterables\Foo', - '$fooFromGenerator', - ], - [ - 'ArrayObject', - '$arrayObject', - ], - [ - 'int', - '$arrayObjectKey', - ], - [ - 'string', - '$arrayObjectValue', - ], - ]; - } - - /** - * @dataProvider dataIterable - * @param string $description - * @param string $expression - */ - public function testIterable( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/iterable.php', - $description, - $expression - ); - } - - public function dataArrayAccess(): array - { - return [ - [ - 'string', - '$this->returnArrayOfStrings()[0]', - ], - [ - 'mixed', - '$this->returnMixed()[0]', - ], - [ - 'int', - '$this->returnSelfWithIterableInt()[0]', - ], - [ - 'int', - '$this[0]', - ], - ]; - } - - /** - * @dataProvider dataArrayAccess - * @param string $description - * @param string $expression - */ - public function testArrayAccess( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/array-accessable.php', - $description, - $expression - ); - } - - public function dataVoid(): array - { - return [ - [ - 'void', - '$this->doFoo()', - ], - [ - 'void', - '$this->doBar()', - ], - [ - 'void', - '$this->doConflictingVoid()', - ], - ]; - } - - /** - * @dataProvider dataVoid - * @param string $description - * @param string $expression - */ - public function testVoid( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/void.php', - $description, - $expression - ); - } - - public function dataNullableReturnTypes(): array - { - return [ - [ - 'int|null', - '$this->doFoo()', - ], - [ - 'int|null', - '$this->doBar()', - ], - [ - 'int|null', - '$this->doConflictingNullable()', - ], - [ - 'int', - '$this->doAnotherConflictingNullable()', - ], - ]; - } - - /** - * @dataProvider dataNullableReturnTypes - * @param string $description - * @param string $expression - */ - public function testNullableReturnTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/nullable-returnTypes.php', - $description, - $expression - ); - } - - public function dataTernary(): array - { - return [ - [ - 'bool|null', - '$boolOrNull', - ], - [ - 'bool', - '$boolOrNull !== null ? $boolOrNull : false', - ], - [ - 'bool', - '$bool', - ], - [ - 'true|null', - '$short', - ], - [ - 'bool', - '$c', - ], - [ - 'bool', - '$isQux', - ], - ]; - } - - /** - * @dataProvider dataTernary - * @param string $description - * @param string $expression - */ - public function testTernary( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/ternary.php', - $description, - $expression - ); - } - - public function dataHeredoc(): array - { - return [ - [ - '\'foo\'', - '$heredoc', - ], - [ - '\'bar\'', - '$nowdoc', - ], - ]; - } - - /** - * @dataProvider dataHeredoc - * @param string $description - * @param string $expression - */ - public function testHeredoc( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/heredoc.php', - $description, - $expression - ); - } - - public function dataTypeElimination(): array - { - return [ - [ - 'null', - '$foo', - "'nullForSure'", - ], - [ - 'TypeElimination\Foo', - '$foo', - "'notNullForSure'", - ], - [ - 'TypeElimination\Foo', - '$foo', - "'notNullForSure2'", - ], - [ - 'null', - '$foo', - "'nullForSure2'", - ], - [ - 'null', - '$foo', - "'nullForSure3'", - ], - [ - 'TypeElimination\Foo', - '$foo', - "'notNullForSure3'", - ], - [ - 'null', - '$foo', - "'yodaNullForSure'", - ], - [ - 'TypeElimination\Foo', - '$foo', - "'yodaNotNullForSure'", - ], - [ - 'false', - '$intOrFalse', - "'falseForSure'", - ], - [ - 'int', - '$intOrFalse', - "'intForSure'", - ], - [ - 'false', - '$intOrFalse', - "'yodaFalseForSure'", - ], - [ - 'int', - '$intOrFalse', - "'yodaIntForSure'", - ], - [ - 'true', - '$intOrTrue', - "'trueForSure'", - ], - [ - 'int', - '$intOrTrue', - "'anotherIntForSure'", - ], - [ - 'true', - '$intOrTrue', - "'yodaTrueForSure'", - ], - [ - 'int', - '$intOrTrue', - "'yodaAnotherIntForSure'", - ], - [ - 'TypeElimination\Foo', - '$fooOrBarOrBaz', - "'fooForSure'", - ], - [ - 'TypeElimination\Bar|TypeElimination\Baz', - '$fooOrBarOrBaz', - "'barOrBazForSure'", - ], - [ - 'TypeElimination\Bar', - '$fooOrBarOrBaz', - "'barForSure'", - ], - [ - 'TypeElimination\Baz', - '$fooOrBarOrBaz', - "'bazForSure'", - ], - [ - 'TypeElimination\Bar|TypeElimination\Baz', - '$fooOrBarOrBaz', - "'anotherBarOrBazForSure'", - ], - [ - 'TypeElimination\Foo', - '$fooOrBarOrBaz', - "'anotherFooForSure'", - ], - [ - 'string|null', - '$result', - "'stringOrNullForSure'", - ], - [ - 'int', - '$intOrFalse', - "'yetAnotherIntForSure'", - ], - [ - 'int', - '$intOrTrue', - "'yetYetAnotherIntForSure'", - ], - [ - 'TypeElimination\Foo|null', - '$fooOrStringOrNull', - "'fooOrNull'", - ], - [ - 'string', - '$fooOrStringOrNull', - "'stringForSure'", - ], - [ - 'string', - '$fooOrStringOrNull', - "'anotherStringForSure'", - ], - [ - 'null', - '$this->bar', - "'propertyNullForSure'", - ], - [ - 'TypeElimination\Bar', - '$this->bar', - "'propertyNotNullForSure'", - ], - ]; - } - - /** - * @dataProvider dataTypeElimination - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testTypeElimination( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/type-elimination.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataMisleadingTypes(): array - { - return [ - [ - 'MisleadingTypes\boolean', - '$foo->misleadingBoolReturnType()', - ], - [ - 'MisleadingTypes\integer', - '$foo->misleadingIntReturnType()', - ], - [ - 'mixed', - '$foo->misleadingMixedReturnType()', - ], - ]; - } - - /** - * @dataProvider dataMisleadingTypes - * @param string $description - * @param string $expression - */ - public function testMisleadingTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/misleading-types.php', - $description, - $expression - ); - } - - public function dataMisleadingTypesWithoutNamespace(): array - { - return [ - [ - 'boolean', // would have been "bool" for a real boolean - '$foo->misleadingBoolReturnType()', - ], - [ - 'integer', - '$foo->misleadingIntReturnType()', - ], - ]; - } - - /** - * @dataProvider dataMisleadingTypesWithoutNamespace - * @param string $description - * @param string $expression - */ - public function testMisleadingTypesWithoutNamespace( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/misleading-types-without-namespace.php', - $description, - $expression - ); - } - - public function dataUnresolvableTypes(): array - { - return [ - [ - 'mixed', - '$arrayWithTooManyArgs', - ], - [ - 'mixed', - '$iterableWithTooManyArgs', - ], - [ - 'Foo', - '$genericFoo', - ], - ]; - } - - /** - * @dataProvider dataUnresolvableTypes - * @param string $description - * @param string $expression - */ - public function testUnresolvableTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/unresolvable-types.php', - $description, - $expression - ); - } - - public function dataCombineTypes(): array - { - return [ - [ - 'string|null', - '$x', - ], - [ - '1|null', - '$y', - ], - ]; - } - - /** - * @dataProvider dataCombineTypes - * @param string $description - * @param string $expression - */ - public function testCombineTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/combine-types.php', - $description, - $expression - ); - } - - public function dataConstants(): array - { - define('ConstantsForNodeScopeResolverTest\\FOO_CONSTANT', 1); - - return [ - [ - '1', - '$foo', - ], - [ - '*ERROR*', - 'NONEXISTENT_CONSTANT', - ], - [ - "'bar'", - '\\BAR_CONSTANT', - ], - [ - 'mixed', - '\\BAZ_CONSTANT', - ], - ]; - } - - /** - * @dataProvider dataConstants - * @param string $description - * @param string $expression - */ - public function testConstants( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/constants.php', - $description, - $expression - ); - } - - public function dataFinally(): array - { - return [ - [ - '1|\'foo\'', - '$integerOrString', - ], - [ - 'FinallyNamespace\BarException|FinallyNamespace\FooException|null', - '$fooOrBarException', - ], - ]; - } - - /** - * @dataProvider dataFinally - * @param string $description - * @param string $expression - */ - public function testFinally( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/finally.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataFinally - * @param string $description - * @param string $expression - */ - public function testFinallyWithEarlyTermination( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/finally-with-early-termination.php', - $description, - $expression - ); - } - - public function dataInheritDocFromInterface(): array - { - return [ - [ - 'string', - '$string', - ], - ]; - } - - /** - * @dataProvider dataInheritDocFromInterface - * @param string $description - * @param string $expression - */ - public function testInheritDocFromInterface( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/inheritdoc-from-interface.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataInheritDocFromInterface - * @param string $description - * @param string $expression - */ - public function testInheritDocWithoutCurlyBracesFromInterface( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface.php', - $description, - $expression - ); - } - - public function dataInheritDocFromInterface2(): array - { - return [ - [ - 'int', - '$int', - ], - ]; - } - - /** - * @dataProvider dataInheritDocFromInterface2 - * @param string $description - * @param string $expression - */ - public function testInheritDocFromInterface2( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/inheritdoc-from-interface2-definition.php'; - $this->assertTypes( - __DIR__ . '/data/inheritdoc-from-interface2.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataInheritDocFromInterface2 - * @param string $description - * @param string $expression - */ - public function testInheritDocWithoutCurlyBracesFromInterface2( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2-definition.php'; - $this->assertTypes( - __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2.php', - $description, - $expression - ); - } - - public function dataInheritDocFromTrait(): array - { - return [ - [ - 'string', - '$string', - ], - ]; - } - - /** - * @dataProvider dataInheritDocFromTrait - * @param string $description - * @param string $expression - */ - public function testInheritDocFromTrait( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/inheritdoc-from-trait.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataInheritDocFromTrait - * @param string $description - * @param string $expression - */ - public function testInheritDocWithoutCurlyBracesFromTrait( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait.php', - $description, - $expression - ); - } - - public function dataInheritDocFromTrait2(): array - { - return [ - [ - 'string', - '$string', - ], - ]; - } - - /** - * @dataProvider dataInheritDocFromTrait2 - * @param string $description - * @param string $expression - */ - public function testInheritDocFromTrait2( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/inheritdoc-from-trait2-definition.php'; - require_once __DIR__ . '/data/inheritdoc-from-trait2-definition2.php'; - $this->assertTypes( - __DIR__ . '/data/inheritdoc-from-trait2.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataInheritDocFromTrait2 - * @param string $description - * @param string $expression - */ - public function testInheritDocWithoutCurlyBracesFromTrait2( - string $description, - string $expression - ): void - { - require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2-definition.php'; - require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2-definition2.php'; - $this->assertTypes( - __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2.php', - $description, - $expression - ); - } - - public function dataResolveStatic(): array - { - return [ - [ - 'ResolveStatic\Foo', - '\ResolveStatic\Foo::create()', - ], - [ - 'ResolveStatic\Bar', - '\ResolveStatic\Bar::create()', - ], - [ - 'array(\'foo\' => ResolveStatic\Bar)', - '$bar->returnConstantArray()', - ], - [ - 'ResolveStatic\Bar|null', - '$bar->nullabilityNotInSync()', - ], - [ - 'ResolveStatic\Bar', - '$bar->anotherNullabilityNotInSync()', - ], - ]; - } - - /** - * @dataProvider dataResolveStatic - * @param string $description - * @param string $expression - */ - public function testResolveStatic( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/resolve-static.php', - $description, - $expression - ); - } - - public function dataLoopVariables(): array - { - return [ - [ - 'LoopVariables\Foo|LoopVariables\Lorem|null', - '$foo', - "'begin'", - ], - [ - 'LoopVariables\Foo', - '$foo', - "'afterAssign'", - ], - [ - 'LoopVariables\Foo', - '$foo', - "'end'", - ], - [ - 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', - '$foo', - "'afterLoop'", - ], - [ - 'int|null', - '$nullableVal', - "'begin'", - ], - [ - 'null', - '$nullableVal', - "'nullableValIf'", - ], - [ - 'int', - '$nullableVal', - "'nullableValElse'", - ], - [ - 'int|null', - '$nullableVal', - "'afterLoop'", - ], - [ - 'LoopVariables\Foo|false', - '$falseOrObject', - "'begin'", - ], - [ - 'LoopVariables\Foo', - '$falseOrObject', - "'end'", - ], - [ - 'LoopVariables\Foo|false', - '$falseOrObject', - "'afterLoop'", - ], - ]; - } - - public function dataForeachLoopVariables(): array - { - return [ - [ - '1|2|3', - '$val', - "'begin'", - ], - [ - '0|1|2', - '$key', - "'begin'", - ], - [ - '1|2|3|null', - '$val', - "'afterLoop'", - ], - [ - '0|1|2|null', - '$key', - "'afterLoop'", - ], - [ - '1|2|3|null', - '$emptyForeachVal', - "'afterLoop'", - ], - [ - '0|1|2|null', - '$emptyForeachKey', - "'afterLoop'", - ], - [ - '1|2|3', - '$nullableInt', - "'end'", - ], - [ - 'array', - '$integers', - "'end'", - ], - [ - 'array', - '$integers', - "'afterLoop'", - ], - [ - 'array', - '$this->property', - "'begin'", - ], - [ - 'array', - '$this->property', - "'end'", - ], - [ - 'array', - '$this->property', - "'afterLoop'", - ], - [ - 'int', - '$i', - "'begin'", - ], - [ - 'int', - '$i', - "'end'", - ], - [ - 'int', - '$i', - "'afterLoop'", - ], - ]; - } - - public function dataWhileLoopVariables(): array - { - return [ - [ - 'int', - '$i', - "'begin'", - ], - [ - 'int', - '$i', - "'end'", - ], - [ - 'int', - '$i', - "'afterLoop'", - ], - ]; - } - - - public function dataForLoopVariables(): array - { - return [ - [ - 'int', - '$i', - "'begin'", - ], - [ - 'int', - '$i', - "'end'", - ], - [ - 'int', - '$i', - "'afterLoop'", - ], - ]; - } - - - - /** - * @dataProvider dataLoopVariables - * @dataProvider dataForeachLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testForeachLoopVariables( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/foreach-loop-variables.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - /** - * @dataProvider dataLoopVariables - * @dataProvider dataWhileLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testWhileLoopVariables( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/while-loop-variables.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - /** - * @dataProvider dataLoopVariables - * @dataProvider dataForLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testForLoopVariables( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/for-loop-variables.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataDoWhileLoopVariables(): array - { - return [ - [ - 'LoopVariables\Foo|LoopVariables\Lorem|null', - '$foo', - "'begin'", - ], - [ - 'LoopVariables\Foo', - '$foo', - "'afterAssign'", - ], - [ - 'LoopVariables\Foo', - '$foo', - "'end'", - ], - [ - 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem', - '$foo', - "'afterLoop'", - ], - [ - 'int', - '$i', - "'begin'", - ], - [ - 'int', - '$i', - "'end'", - ], - [ - 'int', - '$i', - "'afterLoop'", - ], - [ - 'int|null', - '$nullableVal', - "'begin'", - ], - [ - 'null', - '$nullableVal', - "'nullableValIf'", - ], - [ - 'int', - '$nullableVal', - "'nullableValElse'", - ], - [ - 'int', - '$nullableVal', - "'afterLoop'", - ], - [ - 'LoopVariables\Foo|false', - '$falseOrObject', - "'begin'", - ], - [ - 'LoopVariables\Foo', - '$falseOrObject', - "'end'", - ], - [ - 'LoopVariables\Foo|false', - '$falseOrObject', - "'afterLoop'", - ], - [ - 'LoopVariables\Foo|false', - '$anotherFalseOrObject', - "'begin'", - ], - [ - 'LoopVariables\Foo', - '$anotherFalseOrObject', - "'end'", - ], - [ - 'LoopVariables\Foo', - '$anotherFalseOrObject', - "'afterLoop'", - ], - - ]; - } - - /** - * @dataProvider dataDoWhileLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testDoWhileLoopVariables( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/do-while-loop-variables.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataMultipleClassesInOneFile(): array - { - return [ - [ - 'MultipleClasses\Foo', - '$self', - "'Foo'", - ], - [ - 'MultipleClasses\Bar', - '$self', - "'Bar'", - ], - ]; - } - - /** - * @dataProvider dataMultipleClassesInOneFile - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testMultipleClassesInOneFile( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/multiple-classes-per-file.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataCallingMultipleClassesInOneFile(): array - { - return [ - [ - 'MultipleClasses\Foo', - '$foo->returnSelf()', - ], - [ - 'MultipleClasses\Bar', - '$bar->returnSelf()', - ], - ]; - } - - /** - * @dataProvider dataCallingMultipleClassesInOneFile - * @param string $description - * @param string $expression - */ - public function testCallingMultipleClassesInOneFile( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/calling-multiple-classes-per-file.php', - $description, - $expression - ); - } - - public function dataExplode(): array - { - return [ - [ - 'array', - '$sureArray', - ], - [ - 'false', - '$sureFalse', - ], - [ - 'array|false', - '$arrayOrFalse', - ], - [ - 'array|false', - '$anotherArrayOrFalse', - ], - [ - '(array|false)', - '$benevolentArrayOrFalse', - ], - ]; - } - - /** - * @dataProvider dataExplode - * @param string $description - * @param string $expression - */ - public function testExplode( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/explode.php', - $description, - $expression - ); - } - - public function dataArrayPointerFunctions(): array - { - return [ - [ - 'mixed', - 'reset()', - ], - [ - 'stdClass|false', - 'reset($generalArray)', - ], - [ - 'mixed', - 'reset($somethingElse)', - ], - [ - 'false', - 'reset($emptyConstantArray)', - ], - [ - '1', - 'reset($constantArray)', - ], - [ - '\'baz\'|\'foo\'', - 'reset($conditionalArray)', - ], - [ - 'mixed', - 'end()', - ], - [ - 'stdClass|false', - 'end($generalArray)', - ], - [ - 'mixed', - 'end($somethingElse)', - ], - [ - 'false', - 'end($emptyConstantArray)', - ], - [ - '2', - 'end($constantArray)', - ], - [ - '\'bar\'|\'baz\'', - 'end($secondConditionalArray)', - ], - ]; - } - - /** - * @dataProvider dataArrayPointerFunctions - * @param string $description - * @param string $expression - */ - public function testArrayPointerFunctions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/array-pointer-functions.php', - $description, - $expression - ); - } - - public function dataReplaceFunctions(): array - { - return [ - [ - 'string', - '$expectedString', - ], - [ - 'string|null', - '$expectedString2', - ], - [ - 'string|null', - '$anotherExpectedString', - ], - [ - 'array(\'a\' => string, \'b\' => string)', - '$expectedArray', - ], - [ - 'array(\'a\' => string, \'b\' => string)|null', - '$expectedArray2', - ], - [ - 'array(\'a\' => string, \'b\' => string)|null', - '$anotherExpectedArray', - ], - [ - 'array|string', - '$expectedArrayOrString', - ], - [ - '(array|string)', - '$expectedBenevolentArrayOrString', - ], - [ - 'array|string|null', - '$expectedArrayOrString2', - ], - [ - 'array|string|null', - '$anotherExpectedArrayOrString', - ], - [ - 'array(\'a\' => string, \'b\' => string)|null', - 'preg_replace_callback_array($callbacks, $array)', - ], - [ - 'string|null', - 'preg_replace_callback_array($callbacks, $string)', - ], - [ - 'string', - 'str_replace(\'.\', \':\', $intOrStringKey)', - ], - [ - 'string', - 'str_ireplace(\'.\', \':\', $intOrStringKey)', - ], - ]; - } - - /** - * @dataProvider dataReplaceFunctions - * @param string $description - * @param string $expression - */ - public function testReplaceFunctions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/replaceFunctions.php', - $description, - $expression - ); - } - - public function dataFilterVar(): Generator - { - $typesAndFilters = [ - 'string' => [ - 'FILTER_DEFAULT', - 'FILTER_UNSAFE_RAW', - 'FILTER_SANITIZE_EMAIL', - 'FILTER_SANITIZE_ENCODED', - 'FILTER_SANITIZE_NUMBER_FLOAT', - 'FILTER_SANITIZE_NUMBER_INT', - 'FILTER_SANITIZE_SPECIAL_CHARS', - 'FILTER_SANITIZE_STRING', - 'FILTER_SANITIZE_URL', - 'FILTER_VALIDATE_EMAIL', - 'FILTER_VALIDATE_IP', - '$filterIp', - 'FILTER_VALIDATE_MAC', - 'FILTER_VALIDATE_REGEXP', - 'FILTER_VALIDATE_URL', - ], - 'int' => ['FILTER_VALIDATE_INT'], - 'float' => ['FILTER_VALIDATE_FLOAT'], - ]; - - if (defined('FILTER_SANITIZE_MAGIC_QUOTES')) { - $typesAndFilters['string'][] = 'FILTER_SANITIZE_MAGIC_QUOTES'; - } - - if (defined('FILTER_SANITIZE_ADD_SLASHES')) { - $typesAndFilters['string'][] = 'FILTER_SANITIZE_ADD_SLASHES'; - } - - $typeAndFlags = [ - ['%s|false', ''], - ['%s|false', ', $mixed'], - ['%s|false', ', ["flags" => $mixed]'], - ['%s|null', ', FILTER_NULL_ON_FAILURE'], - ['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'], - ['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'], - ['%s|null', ', ["flags" => $nullFilter]'], - ['Analyser|%s', ', ["options" => ["default" => new Analyser]]'], - ['array<%s|null>', ', FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY'], - ['array<%s|null>', ', FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY | FILTER_FLAG_IPV4'], - ['array<%s|false>', ', FILTER_FORCE_ARRAY'], - ['array<%s|null>', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], - ['array<%s|false>', ', ["flags" => FILTER_FORCE_ARRAY | FILTER_FLAG_IPV4]'], - ['array<%s|false>', ', ["flags" => $forceArrayFilter]'], - ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_FORCE_ARRAY]'], - ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], - ]; - - foreach ($typesAndFilters as $filterType => $filters) { - foreach ($filters as $filter) { - foreach ($typeAndFlags as [$type, $flag]) { - yield [ - sprintf($type, $filterType), - sprintf('filter_var($mixed, %s %s)', $filter, $flag), - ]; - } - } - } - - $boolFlags = [ - ['bool', ''], - ['bool', ', $mixed'], - ['bool', ', ["flags" => $mixed]'], - ['bool|null', ', FILTER_NULL_ON_FAILURE'], - ['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'], - ['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'], - ['bool|null', ', ["flags" => $nullFilter]'], - ['Analyser|bool', ', ["options" => ["default" => new Analyser]]'], - ['bool', ', ["options" => ["default" => true]]'], - ['array', ', FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY'], - ['array', ', FILTER_FORCE_ARRAY'], - ['array', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], - ['array', ', ["flags" => $forceArrayFilter]'], - ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_FORCE_ARRAY]'], - ['array',', ["options" => ["default" => false], "flags" => FILTER_FORCE_ARRAY]'], - ['array',', ["options" => ["default" => new Analyser], "flags" => FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY]'], - ]; - - foreach ($boolFlags as [$type, $flags]) { - yield [ - $type, - sprintf('filter_var($mixed, FILTER_VALIDATE_BOOLEAN %s)', $flags), - ]; - } - - //edge cases - yield 'unknown filter' => [ - 'mixed', - 'filter_var($mixed, $mixed)', - ]; - - yield 'default that is the same type as result' => [ - 'string', - 'filter_var($mixed, FILTER_SANITIZE_URL, ["options" => ["default" => "foo"]])', - ]; - - yield 'no second variable' => [ - 'string|false', - 'filter_var($mixed)', - ]; - } - - public function dataFilterVarUnchanged(): array - { - return [ - [ - '12', - 'filter_var(12, FILTER_VALIDATE_INT)', - ], - [ - 'false', - 'filter_var(false, FILTER_VALIDATE_BOOLEAN)', - ], - [ - 'array', - 'filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY)', - ], - [ - 'array', - 'filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE)', - ], - [ - '3.27', - 'filter_var(3.27, FILTER_VALIDATE_FLOAT)', - ], - [ - '3.27', - 'filter_var(3.27, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE)', - ], - [ - 'int', - 'filter_var(rand(), FILTER_VALIDATE_INT)', - ], - [ - '12.0', - 'filter_var(12, FILTER_VALIDATE_FLOAT)', - ], - ]; - } - - /** - * @dataProvider dataFilterVar - * @dataProvider dataFilterVarUnchanged - * @param string $description - * @param string $expression - */ - public function testFilterVar( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/filterVar.php', - $description, - $expression - ); - } - - public function dataClosureWithUsePassedByReference(): array - { - return [ - [ - 'false', - '$progressStarted', - "'beforeCallback'", - ], - [ - 'false', - '$anotherVariable', - "'beforeCallback'", - ], - [ - '1|bool', - '$progressStarted', - "'inCallbackBeforeAssign'", - ], - [ - 'false', - '$anotherVariable', - "'inCallbackBeforeAssign'", - ], - [ - 'null', - '$untouchedPassedByRef', - "'inCallbackBeforeAssign'", - ], - [ - '1|true', - '$progressStarted', - "'inCallbackAfterAssign'", - ], - [ - 'true', - '$anotherVariable', - "'inCallbackAfterAssign'", - ], - [ - '1|bool', - '$progressStarted', - "'afterCallback'", - ], - [ - 'false', - '$anotherVariable', - "'afterCallback'", - ], - [ - 'null', - '$untouchedPassedByRef', - "'afterCallback'", - ], - [ - '1', - '$incrementedInside', - "'beforeCallback'", - ], - [ - 'int', - '$incrementedInside', - "'inCallbackBeforeAssign'", - ], - [ - 'int', - '$incrementedInside', - "'inCallbackAfterAssign'", - ], - [ - 'int', - '$incrementedInside', - "'afterCallback'", - ], - [ - 'null', - '$fooOrNull', - "'beforeCallback'", - ], - [ - 'ClosurePassedByReference\Foo|null', - '$fooOrNull', - "'inCallbackBeforeAssign'", - ], - [ - 'ClosurePassedByReference\Foo', - '$fooOrNull', - "'inCallbackAfterAssign'", - ], - [ - 'ClosurePassedByReference\Foo|null', - '$fooOrNull', - "'afterCallback'", - ], - ]; - } - - /** - * @dataProvider dataClosureWithUsePassedByReference - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testClosureWithUsePassedByReference( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/closure-passed-by-reference.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataClosureWithUsePassedByReferenceInMethodCall(): array - { - return [ - [ - 'int|null', - '$five', - ], - ]; - } - - /** - * @dataProvider dataClosureWithUsePassedByReferenceInMethodCall - * @param string $description - * @param string $expression - */ - public function testClosureWithUsePassedByReferenceInMethodCall( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/closure-passed-by-reference-in-call.php', - $description, - $expression - ); - } - - public function dataClosureWithUsePassedByReferenceReturn(): array - { - return [ - [ - 'null', - '$fooOrNull', - "'beforeCallback'", - ], - [ - 'ClosurePassedByReference\Foo|null', - '$fooOrNull', - "'inCallbackBeforeAssign'", - ], - [ - 'ClosurePassedByReference\Foo', - '$fooOrNull', - "'inCallbackAfterAssign'", - ], - [ - 'ClosurePassedByReference\Foo|null', - '$fooOrNull', - "'afterCallback'", - ], - ]; - } - - public function dataStaticClosure(): array - { - return [ - [ - '*ERROR*', - '$this', - ], - ]; - } - - /** - * @dataProvider dataStaticClosure - * @param string $description - * @param string $expression - */ - public function testStaticClosure( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/static-closure.php', - $description, - $expression - ); - } - - /** - * @dataProvider dataClosureWithUsePassedByReferenceReturn - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testClosureWithUsePassedByReferenceReturn( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/closure-passed-by-reference-return.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataClosureWithInferredTypehint(): array - { - return [ - [ - 'DateTime|stdClass', - '$foo', - ], - [ - 'mixed', - '$bar', - ], - ]; - } - - /** - * @dataProvider dataClosureWithInferredTypehint - * @param string $description - * @param string $expression - */ - public function testClosureWithInferredTypehint( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/closure-inferred-typehint.php', - $description, - $expression, - [], - [], - [], - [], - 'die', - [], - false - ); - } - - public function dataTraitsPhpDocs(): array - { - return [ - [ - 'mixed', - '$this->propertyWithoutPhpDoc', - ], - [ - 'TraitPhpDocsTwo\TraitPropertyType', - '$this->traitProperty', - ], - [ - 'TraitPhpDocs\PropertyTypeFromClass', - '$this->conflictingProperty', - ], - [ - 'TraitPhpDocs\AmbiguousPropertyType', - '$this->bogusProperty', - ], - [ - 'TraitPhpDocs\BogusPropertyType', - '$this->anotherBogusProperty', - ], - [ - 'TraitPhpDocsTwo\BogusPropertyType', - '$this->differentBogusProperty', - ], - [ - 'string', - '$this->methodWithoutPhpDoc()', - ], - [ - 'TraitPhpDocsTwo\TraitMethodType', - '$this->traitMethod()', - ], - [ - 'TraitPhpDocs\MethodTypeFromClass', - '$this->conflictingMethod()', - ], - [ - 'TraitPhpDocs\AmbiguousMethodType', - '$this->bogusMethod()', - ], - [ - 'TraitPhpDocs\BogusMethodType', - '$this->anotherBogusMethod()', - ], - [ - 'TraitPhpDocsTwo\BogusMethodType', - '$this->differentBogusMethod()', - ], - [ - 'TraitPhpDocsTwo\DuplicateMethodType', - '$this->methodInMoreTraits()', - ], - [ - 'TraitPhpDocsThree\AnotherDuplicateMethodType', - '$this->anotherMethodInMoreTraits()', - ], - [ - 'TraitPhpDocsTwo\YetAnotherDuplicateMethodType', - '$this->yetAnotherMethodInMoreTraits()', - ], - [ - 'TraitPhpDocsThree\YetAnotherDuplicateMethodType', - '$this->aliasedYetAnotherMethodInMoreTraits()', - ], - [ - 'TraitPhpDocsThree\YetYetAnotherDuplicateMethodType', - '$this->yetYetAnotherMethodInMoreTraits()', - ], - [ - 'TraitPhpDocsTwo\YetYetAnotherDuplicateMethodType', - '$this->aliasedYetYetAnotherMethodInMoreTraits()', - ], - [ - 'int', - '$this->propertyFromTraitUsingTrait', - ], - [ - 'string', - '$this->methodFromTraitUsingTrait()', - ], - [ - 'TraitPhpDocsThree\Foo', - '$this->loremTraitProperty', - ], - ]; - } - - /** - * @dataProvider dataTraitsPhpDocs - * @param string $description - * @param string $expression - */ - public function testTraitsPhpDocs( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/traits/traits.php', - $description, - $expression - ); - } - - public function dataPassedByReference(): array - { - return [ - [ - 'array(1, 2, 3)', - '$arr', - ], - [ - 'mixed', - '$matches', - ], - [ - 'mixed', - '$s', - ], - ]; - } - - /** - * @dataProvider dataPassedByReference - * @param string $description - * @param string $expression - */ - public function testPassedByReference( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/passed-by-reference.php', - $description, - $expression - ); - } - - public function dataCallables(): array - { - return [ - [ - 'int', - '$foo()', - ], - [ - 'string', - '$closure()', - ], - [ - 'Callables\\Bar', - '$arrayWithStaticMethod()', - ], - [ - 'float', - '$stringWithStaticMethod()', - ], - [ - 'float', - '$arrayWithInstanceMethod()', - ], - [ - 'mixed', - '$closureObject()', - ], - ]; - } - - /** - * @dataProvider dataCallables - * @param string $description - * @param string $expression - */ - public function testCallables( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/callables.php', - $description, - $expression - ); - } - - public function dataArrayKeysInBranches(): array - { - return [ - [ - 'array(\'i\' => int, \'j\' => int, \'k\' => int, \'key\' => DateTimeImmutable, \'l\' => 1, \'m\' => 5, ?\'n\' => \'str\')', - '$array', - ], - [ - 'array', - '$generalArray', - ], - [ - 'mixed', // should be DateTimeImmutable - '$generalArray[\'key\']', - ], - [ - 'array(0 => \'foo\', 1 => \'bar\', ?2 => \'baz\')', - '$arrayAppendedInIf', - ], - [ - 'array', - '$arrayAppendedInForeach', - ], - [ - 'array', - '$anotherArrayAppendedInForeach', - ], - [ - '\'str\'', - '$array[\'n\']', - ], - [ - 'int', - '$incremented', - ], - [ - '0|1', - '$setFromZeroToOne', - ], - ]; - } - - /** - * @dataProvider dataArrayKeysInBranches - * @param string $description - * @param string $expression - */ - public function testArrayKeysInBranches( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/array-keys-branches.php', - $description, - $expression - ); - } - - public function dataSpecifiedFunctionCall(): array - { - return [ - [ - 'true', - 'is_file($autoloadFile)', - "'first'", - ], - [ - 'true', - 'is_file($autoloadFile)', - "'second'", - ], - [ - 'true', - 'is_file($autoloadFile)', - "'third'", - ], - [ - 'bool', - 'is_file($autoloadFile)', - "'fourth'", - ], - [ - 'true', - 'is_file($autoloadFile)', - "'fifth'", - ], - ]; - } - - /** - * @dataProvider dataSpecifiedFunctionCall - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testSpecifiedFunctionCall( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/specified-function-call.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataElementsOnMixed(): array - { - return [ - [ - 'mixed', - '$mixed->foo', - ], - [ - 'mixed', - '$mixed->foo->bar', - ], - [ - 'mixed', - '$mixed->foo()', - ], - [ - 'mixed', - '$mixed->foo()->bar()', - ], - [ - 'mixed', - '$mixed::TEST_CONSTANT', - ], - ]; - } - - /** - * @dataProvider dataElementsOnMixed - * @param string $description - * @param string $expression - */ - public function testElementsOnMixed( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/mixed-elements.php', - $description, - $expression - ); - } - - public function dataCaseInsensitivePhpDocTypes(): array - { - return [ - [ - 'Foo\Bar', - '$this->bar', - ], - [ - 'Foo\Baz', - '$this->lorem', - ], - ]; - } - - /** - * @dataProvider dataCaseInsensitivePhpDocTypes - * @param string $description - * @param string $expression - */ - public function testCaseInsensitivePhpDocTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/case-insensitive-phpdocs.php', - $description, - $expression - ); - } - - public function dataConstantTypeAfterDuplicateCondition(): array - { - return [ - [ - '0', - '$a', - "'inCondition'", - ], - [ - '0', - '$b', - "'inCondition'", - ], - [ - '0', - '$c', - "'inCondition'", - ], - [ - 'int', - '$a', - "'afterFirst'", - ], - [ - 'int', - '$b', - "'afterFirst'", - ], - [ - '0', - '$c', - "'afterFirst'", - ], - [ - 'int', - '$a', - "'afterSecond'", - ], - [ - 'int', - '$b', - "'afterSecond'", - ], - [ - '0', - '$c', - "'afterSecond'", - ], - [ - 'int', - '$a', - "'afterThird'", - ], - [ - 'int', - '$b', - "'afterThird'", - ], - [ - '0', - '$c', - "'afterThird'", - ], - ]; - } - - /** - * @dataProvider dataConstantTypeAfterDuplicateCondition - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testConstantTypeAfterDuplicateCondition( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/constant-types-duplicate-condition.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataAnonymousClass(): array - { - return [ - [ - '$this(AnonymousClass3301acd9e9d13ba9bbce9581cdb00699)', - '$this', - "'inside'", - ], - [ - 'AnonymousClass3301acd9e9d13ba9bbce9581cdb00699', - '$foo', - "'outside'", - ], - [ - 'AnonymousClassName\Foo', - '$this->fooProperty', - "'inside'", - ], - [ - 'AnonymousClassName\Foo', - '$foo->fooProperty', - "'outside'", - ], - [ - 'AnonymousClassName\Foo', - '$this->doFoo()', - "'inside'", - ], - [ - 'AnonymousClassName\Foo', - '$foo->doFoo()', - "'outside'", - ], - ]; - } - - /** - * @dataProvider dataAnonymousClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testAnonymousClassName( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/anonymous-class-name.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataAnonymousClassInTrait(): array - { - return [ - [ - '$this(AnonymousClass3de0a9734314db9dec21ba308363ff9a)', - '$this', - ], - ]; - } - - /** - * @dataProvider dataAnonymousClassInTrait - * @param string $description - * @param string $expression - */ - public function testAnonymousClassNameInTrait( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/anonymous-class-name-in-trait.php', - $description, - $expression - ); - } - - public function dataDynamicConstants(): array - { - return [ - [ - 'string', - 'DynamicConstants\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', - ], - [ - "'abc123def'", - 'DynamicConstants\DynamicConstantClass::PURE_CONSTANT_IN_CLASS', - ], - [ - "'xyz'", - 'DynamicConstants\NoDynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', - ], - [ - 'bool', - 'GLOBAL_DYNAMIC_CONSTANT', - ], - [ - '123', - 'GLOBAL_PURE_CONSTANT', - ], - ]; - } - - /** - * @dataProvider dataDynamicConstants - * @param string $description - * @param string $expression - */ - public function testDynamicConstants( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/dynamic-constant.php', - $description, - $expression, - [], - [], - [], - [], - 'die', - [ - 'DynamicConstants\\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', - 'GLOBAL_DYNAMIC_CONSTANT', - ] - ); - } - - public function dataIsset(): array - { - return [ - [ - '2|3', - '$array[\'b\']', - ], - [ - 'array(\'a\' => 1|2|3, \'b\' => 2|3, ?\'c\' => 4)', - '$array', - ], - [ - 'array(\'a\' => 1|2|3, \'b\' => 2|3|null, ?\'c\' => 4)', - '$arrayCopy', - ], - [ - 'array(\'a\' => 1|2|3, ?\'c\' => 4)', - '$anotherArrayCopy', - ], - [ - 'array', - '$yetAnotherArrayCopy', - ], - [ - 'mixed~null', - '$mixedIsset', - ], - [ - 'array&hasOffset(\'a\')', - '$mixedArrayKeyExists', - ], - [ - 'array&hasOffset(\'a\')', - '$integers', - ], - [ - 'int', - '$integers[\'a\']', - ], - [ - 'false', - '$lookup[\'derp\'] ?? false', - ], - [ - 'true', - '$lookup[\'foo\'] ?? false', - ], - [ - 'bool', - '$lookup[$a] ?? false', - ], - [ - '\'foo\'|false', - '$nullableArray[\'a\'] ?? false', - ], - [ - '\'bar\'', - '$nullableArray[\'b\'] ?? false', - ], - [ - '\'baz\'|false', - '$nullableArray[\'c\'] ?? false', - ], - ]; - } - - /** - * @dataProvider dataIsset - * @param string $description - * @param string $expression - */ - public function testIsset( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/isset.php', - $description, - $expression - ); - } - - public function dataPropertyArrayAssignment(): array - { - return [ - [ - 'mixed', - '$this->property', - "'start'", - ], - [ - 'array()', - '$this->property', - "'emptyArray'", - ], - [ - '*ERROR*', - '$this->property[\'foo\']', - "'emptyArray'", - ], - [ - 'array(\'foo\' => 1)', - '$this->property', - "'afterAssignment'", - ], - [ - '1', - '$this->property[\'foo\']', - "'afterAssignment'", - ], - ]; - } - - /** - * @dataProvider dataPropertyArrayAssignment - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testPropertyArrayAssignment( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/property-array.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataInArray(): array - { - return [ - [ - '\'bar\'|\'foo\'', - '$s', - ], - [ - 'string', - '$mixed', - ], - [ - 'string', - '$r', - ], - [ - '\'foo\'', - '$fooOrBarOrBaz', - ], - ]; - } - - /** - * @dataProvider dataInArray - * @param string $description - * @param string $expression - */ - public function testInArray( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/in-array.php', - $description, - $expression - ); - } - - public function dataGetParentClass(): array - { - return [ - [ - 'false', - 'get_parent_class()', - ], - [ - 'class-string|false', - 'get_parent_class($s)', - ], - [ - 'false', - 'get_parent_class(\ParentClass\Foo::class)', - ], - [ - 'class-string|false', - 'get_parent_class(NonexistentClass::class)', - ], - [ - 'class-string|false', - 'get_parent_class(1)', - ], - [ - "'ParentClass\\\\Foo'", - 'get_parent_class(\ParentClass\Bar::class)', - ], - [ - 'false', - 'get_parent_class()', - "'inParentClass'", - ], - [ - 'false', - 'get_parent_class($this)', - "'inParentClass'", - ], - [ - 'class-string', - 'get_class($this)', - "'inParentClass'", - ], - [ - '\'ParentClass\\\\Foo\'', - 'get_class()', - "'inParentClass'", - ], - [ - 'false', - 'get_class()', - ], - [ - "'ParentClass\\\\Foo'", - 'get_parent_class()', - "'inChildClass'", - ], - [ - "'ParentClass\\\\Foo'", - 'get_parent_class($this)', - "'inChildClass'", - ], - [ - 'class-string|false', - 'get_parent_class()', - "'inTrait'", - ], - [ - 'class-string|false', - 'get_parent_class($this)', - "'inTrait'", - ], - ]; - } - - /** - * @dataProvider dataGetParentClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testGetParentClass( - string $description, - string $expression, - string $evaluatedPointExpression = 'die' - ): void - { - $this->assertTypes( - __DIR__ . '/data/get-parent-class.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataIsCountable(): array - { - return [ - [ - 'array|Countable', - '$union', - "'is'", - ], - [ - 'string', - '$union', - "'is_not'", - ], - ]; - } - - /** - * @dataProvider dataIsCountable - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testIsCountable( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - $this->assertTypes( - __DIR__ . '/data/is_countable.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression - ); - } - - public function dataPhp73Functions(): array - { - return [ - [ - 'string|false', - 'json_encode($mixed)', - ], - [ - 'string', - 'json_encode($mixed, JSON_THROW_ON_ERROR)', - ], - [ - 'string', - 'json_encode($mixed, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], - [ - 'string', - 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], - [ - 'mixed', - 'json_decode($mixed)', - ], - [ - 'mixed~false', - 'json_decode($mixed, false, 512, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], - [ - 'mixed~false', - 'json_decode($mixed, false, 512, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', - ], - [ - 'int|string|null', - 'array_key_first($mixedArray)', - ], - [ - 'int|string|null', - 'array_key_last($mixedArray)', - ], - [ - 'int|string', - 'array_key_first($nonEmptyArray)', - ], - [ - 'int|string', - 'array_key_last($nonEmptyArray)', - ], - [ - 'string|null', - 'array_key_first($arrayWithStringKeys)', - ], - [ - 'string|null', - 'array_key_last($arrayWithStringKeys)', - ], - [ - 'null', - 'array_key_first($emptyArray)', - ], - [ - 'null', - 'array_key_last($emptyArray)', - ], - [ - '0', - 'array_key_first($literalArray)', - ], - [ - '2', - 'array_key_last($literalArray)', - ], - [ - '0', - 'array_key_first($anotherLiteralArray)', - ], - [ - '2|3', - 'array_key_last($anotherLiteralArray)', - ], - [ - 'array(int, int)', - '$hrtime1', - ], - [ - 'array(int, int)', - '$hrtime2', - ], - [ - 'float|int', - '$hrtime3', - ], - [ - 'array(int, int)|float|int', - '$hrtime4', - ], - ]; - } - - /** - * @dataProvider dataPhp73Functions - * @param string $description - * @param string $expression - */ - public function testPhp73Functions( - string $description, - string $expression - ): void - { - if (PHP_VERSION_ID < 70300) { - $this->markTestSkipped('Test requires PHP 7.3'); - } - $this->assertTypes( - __DIR__ . '/data/php73_functions.php', - $description, - $expression - ); - } - - public function dataUnionMethods(): array - { - return [ - [ - 'UnionMethods\Bar|UnionMethods\Foo', - '$something->doSomething()', - ], - [ - 'UnionMethods\Bar|UnionMethods\Foo', - '$something::doSomething()', - ], - ]; - } - - /** - * @dataProvider dataUnionMethods - * @param string $description - * @param string $expression - */ - public function testUnionMethods( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/union-methods.php', - $description, - $expression - ); - } - - public function dataUnionProperties(): array - { - return [ - [ - 'UnionProperties\Bar|UnionProperties\Foo', - '$something->doSomething', - ], - [ - 'UnionProperties\Bar|UnionProperties\Foo', - '$something::$doSomething', - ], - ]; - } - - /** - * @dataProvider dataUnionProperties - * @param string $description - * @param string $expression - */ - public function testUnionProperties( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/union-properties.php', - $description, - $expression - ); - } - - public function dataAssignmentInCondition(): array - { - return [ - [ - 'AssignmentInCondition\Foo', - '$bar', - ], - ]; - } - - /** - * @dataProvider dataAssignmentInCondition - * @param string $description - * @param string $expression - */ - public function testAssignmentInCondition( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/assignment-in-condition.php', - $description, - $expression - ); - } - - public function dataGeneralizeScope(): array - { - return [ - [ - "array int, 'loadCount' => int, 'removeCount' => int, 'saveCount' => int)>>", - '$statistics', - ], - ]; - } - - /** - * @dataProvider dataGeneralizeScope - * @param string $description - * @param string $expression - */ - public function testGeneralizeScope( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/generalize-scope.php', - $description, - $expression - ); - } - - public function dataGeneralizeScopeRecursiveType(): array - { - return [ - [ - 'array()|array(\'foo\' => array)', - '$data', - ], - ]; - } - - /** - * @dataProvider dataGeneralizeScopeRecursiveType - * @param string $description - * @param string $expression - */ - public function testGeneralizeScopeRecursiveType( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/generalize-scope-recursive.php', - $description, - $expression - ); - } - - public function dataArrayShapesInPhpDoc(): array - { - return [ - [ - 'array(0 => string, 1 => ArrayShapesInPhpDoc\Foo, \'foo\' => ArrayShapesInPhpDoc\Bar, 2 => ArrayShapesInPhpDoc\Baz)', - '$one', - ], - [ - 'array(0 => string, ?1 => ArrayShapesInPhpDoc\Foo, ?\'foo\' => ArrayShapesInPhpDoc\Bar)', - '$two', - ], - [ - 'array(?0 => string, ?1 => ArrayShapesInPhpDoc\Foo, ?\'foo\' => ArrayShapesInPhpDoc\Bar)', - '$three', - ], - ]; - } - - /** - * @dataProvider dataArrayShapesInPhpDoc - * @param string $description - * @param string $expression - */ - public function testArrayShapesInPhpDoc( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/array-shapes.php', - $description, - $expression - ); - } - - public function dataBug2574(): array - { - require_once __DIR__ . '/data/bug2574.php'; - - return $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php'); - } - - public function dataBug2577(): array - { - require_once __DIR__ . '/data/bug2577.php'; - - return $this->gatherAssertTypes(__DIR__ . '/data/bug2577.php'); - } - - public function dataGenerics(): array - { - require_once __DIR__ . '/data/generics.php'; - - return $this->gatherAssertTypes(__DIR__ . '/data/generics.php'); - } - - public function dataGenericClassStringType(): array - { - require_once __DIR__ . '/data/generic-class-string.php'; - - return $this->gatherAssertTypes(__DIR__ . '/data/generic-class-string.php'); - } - - public function dataInstanceOf(): array - { - require_once __DIR__ . '/data/instanceof.php'; - - return $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php'); - } - - public function dataIntegerRangeTypes(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); - } - - public function dataRandomInt(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); - } - - public function dataClosureReturnTypes(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); - } - - public function dataArrayKey(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/array-key.php'); - } - - public function dataIntersectionStatic(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php'); - } - - public function dataStaticProperties(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php'); - } - - public function dataStaticMethods(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/static-methods.php'); - } - - public function dataBug2612(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2612.php'); - } - - public function dataBug2677(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2677.php'); - } - - public function dataBug2676(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2676.php'); - } - - public function dataPsalmPrefixedTagsWithUnresolvableTypes(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/psalm-prefix-unresolvable.php'); - } - - public function dataComplexGenericsExample(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/complex-generics-example.php'); - } - - public function dataBug2648(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2648.php'); - } - - public function dataBug2740(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2740.php'); - } - - public function dataBug2822(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2822.php'); - } - - public function dataPhpDocInheritanceParameterRemapping(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-parameter-remapping.php'); - } - - public function dataPhpDocInheritanceConstructors(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-constructors.php'); - } - - public function dataListType(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/list-type.php'); - } - - public function dataBug2835(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2835.php'); - } - - public function dataBug2443(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2443.php'); - } - - public function dataBug2750(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2750.php'); - } - - public function dataBug2850(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2850.php'); - } - - public function dataBug2863(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2863.php'); - } - - public function dataNativeTypes(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); - } - - public function dataTypeChangeAfterArrayAccessAssignment(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/type-change-after-array-access-assignment.php'); - } - - public function dataIteratorToArray(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/iterator_to_array.php'); - } +use PHPStan\Testing\TypeInferenceTestCase; +use stdClass; +use function array_shift; +use function define; +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 dataExtDs(): array + /** + * @return iterable + */ + private static function findTestFiles(): iterable { - if (!extension_loaded('ds')) { - return []; + foreach (self::findTestDataFilesFromDirectory(__DIR__ . '/nsrt') as $testFile) { + yield $testFile; } - return $this->gatherAssertTypes(__DIR__ . '/data/ext-ds.php'); - } - - public function dataArrowFunctionReturnTypeInference(): array - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - return []; + if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/data/enum-reflection-php81.php'; } - return $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-return-type.php'); - } - - public function dataIsNumeric(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/is-numeric.php'); - } - - public function dataBug3142(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3142.php'); - } - - public function dataArrayShapeKeysStrings(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/array-shapes-keys-strings.php'); - } - - public function dataBug1216(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php'); - } - - public function dataConstExprPhpDocType(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/const-expr-phpdoc-type.php'); - } - - public function dataBug3226(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3226.php'); - } + if (PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80400) { + yield __DIR__ . '/data/enum-reflection-backed.php'; + } - public function dataBug2001(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2001.php'); - } + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/bug-4902.php'; + } - public function dataBug2232(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2232.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 __DIR__ . '/data/mb-strlen-php73.php'; + } + } - public function dataBug3009(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3009.php'); - } + yield __DIR__ . '/../Rules/Methods/data/bug-6856.php'; - public function dataInheritPhpDocMerging(): array - { - return array_merge( - $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-var.php'), - $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-param.php'), - $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-return.php'), - $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-template.php') - ); - } + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/explode-php74.php'; + } else { + yield __DIR__ . '/data/explode-php80.php'; + } - public function dataBug3266(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3266.php'); - } + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Reflection/data/unionTypes.php'; + yield __DIR__ . '/../Reflection/data/mixedType.php'; + yield __DIR__ . '/../Reflection/data/staticReturnType.php'; + } - public function dataBug3269(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3269.php'); - } + if (PHP_INT_SIZE === 8) { + yield __DIR__ . '/data/predefined-constants-64bit.php'; + } else { + yield __DIR__ . '/data/predefined-constants-32bit.php'; + } - public function dataAssignNestedArray(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/assign-nested-arrays.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 >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-4857.php'; + } - public function dataBug3276(): array - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - return []; + yield __DIR__ . '/../Rules/Methods/data/bug-5089.php'; + yield __DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.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) { + 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/bug-12432-nullable-enum.php'; + yield __DIR__ . '/data/new-in-initializers-runtime.php'; + yield __DIR__ . '/data/scope-in-enum-match-arm-body.php'; } - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3276.php'); - } - public function dataShadowedTraitMethods(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/shadowed-trait-methods.php'); - } + yield __DIR__ . '/data/bug-12432-nullable-int.php'; - public function dataConstInFunctions(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions.php'); - } + yield __DIR__ . '/../Rules/Comparison/data/bug-6473.php'; - public function dataConstInFunctionsNamespaced(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions-namespaced.php'); - } + yield __DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'; - public function dataRootScopeMaybeDefined(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/root-scope-maybe-defined.php'); - } + yield __DIR__ . '/../Rules/Methods/data/bug-5749.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-5757.php'; - public function dataBug3336(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-3336.php'); - } + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Methods/data/bug-6635.php'; + } - public function dataCatchWithoutVariable(): array - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { - return []; + if (PHP_VERSION_ID >= 80300) { + yield __DIR__ . '/../Rules/Constants/data/bug-10212.php'; } - return $this->gatherAssertTypes(__DIR__ . '/data/catch-without-variable.php'); - } - public function dataMixedTypehint(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/mixed-typehint.php'); - } + yield __DIR__ . '/../Rules/Methods/data/bug-3284.php'; - public function dataVariadics(): array - { - return $this->gatherAssertTypes(__DIR__ . '/data/bug-2600.php'); - } + if (PHP_VERSION_ID >= 80300) { + yield __DIR__ . '/../Rules/Methods/data/return-type-class-constant.php'; + } - /** - * @dataProvider dataBug2574 - * @dataProvider dataBug2577 - * @dataProvider dataGenerics - * @dataProvider dataGenericClassStringType - * @dataProvider dataInstanceOf - * @dataProvider dataIntegerRangeTypes - * @dataProvider dataRandomInt - * @dataProvider dataClosureReturnTypes - * @dataProvider dataArrayKey - * @dataProvider dataIntersectionStatic - * @dataProvider dataStaticProperties - * @dataProvider dataStaticMethods - * @dataProvider dataBug2612 - * @dataProvider dataBug2677 - * @dataProvider dataBug2676 - * @dataProvider dataPsalmPrefixedTagsWithUnresolvableTypes - * @dataProvider dataComplexGenericsExample - * @dataProvider dataBug2648 - * @dataProvider dataBug2740 - * @dataProvider dataPhpDocInheritanceParameterRemapping - * @dataProvider dataPhpDocInheritanceConstructors - * @dataProvider dataListType - * @dataProvider dataBug2822 - * @dataProvider dataBug2835 - * @dataProvider dataBug2443 - * @dataProvider dataBug2750 - * @dataProvider dataBug2850 - * @dataProvider dataBug2863 - * @dataProvider dataNativeTypes - * @dataProvider dataTypeChangeAfterArrayAccessAssignment - * @dataProvider dataIteratorToArray - * @dataProvider dataExtDs - * @dataProvider dataArrowFunctionReturnTypeInference - * @dataProvider dataIsNumeric - * @dataProvider dataBug3142 - * @dataProvider dataArrayShapeKeysStrings - * @dataProvider dataBug1216 - * @dataProvider dataConstExprPhpDocType - * @dataProvider dataBug3226 - * @dataProvider dataBug2001 - * @dataProvider dataBug2232 - * @dataProvider dataBug3009 - * @dataProvider dataInheritPhpDocMerging - * @dataProvider dataBug3266 - * @dataProvider dataBug3269 - * @dataProvider dataAssignNestedArray - * @dataProvider dataBug3276 - * @dataProvider dataShadowedTraitMethods - * @dataProvider dataConstInFunctions - * @dataProvider dataConstInFunctionsNamespaced - * @dataProvider dataRootScopeMaybeDefined - * @dataProvider dataBug3336 - * @dataProvider dataCatchWithoutVariable - * @dataProvider dataMixedTypehint - * @dataProvider dataVariadics - * @param ConstantStringType $expectedType - * @param Type $actualType - */ - public function testFileAsserts( - string $file, - ConstantStringType $expectedType, - Type $actualType, - int $line - ): void - { - $expected = $expectedType->getValue(); - $actual = $actualType->describe(VerbosityLevel::precise()); - $this->assertSame( - $expected, - $actual, - sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $line) - ); - } + //define('ALREADY_DEFINED_CONSTANT', true); + //yield from $this->gatherAssertTypes(__DIR__ . '/data/already-defined-constant.php'); - /** - * @param string $file - * @return array - */ - private function gatherAssertTypes(string $file): array - { - $types = []; - $this->processFile($file, function (Node $node, Scope $scope) use (&$types, $file): void { - if (!$node instanceof Node\Expr\FuncCall) { - return; - } + yield __DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'; - $nameNode = $node->name; - if (!$nameNode instanceof Name) { - return; - } + 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'; - $functionName = $nameNode->toString(); - if ($functionName === 'PHPStan\\Analyser\\assertType') { - $assertTypeFunctionName = 'PHPStan\\Analyser\\assertType'; - $expectedType = $scope->getType($node->args[0]->value); - $actualType = $scope->getType($node->args[1]->value); - } elseif ($functionName === 'PHPStan\\Analyser\\assertNativeType') { - $assertTypeFunctionName = 'PHPStan\\Analyser\\assertNativeType'; - $expectedType = $scope->getNativeType($node->args[0]->value); - $actualType = $scope->getNativeType($node->args[1]->value); - } else { - return; - } + yield __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'; - if (count($node->args) !== 2) { - $this->fail(sprintf( - 'ERROR: Wrong %s() call on line %d.', - $assertTypeFunctionName, - $node->getLine() - )); - } + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/../Rules/Methods/data/true-typehint.php'; + } + yield __DIR__ . '/../Rules/Arrays/data/bug-6000.php'; - $types[$file . ':' . $node->getLine()] = [$file, $expectedType, $actualType, $node->getLine()]; - }); + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php'; + yield __DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php'; - return $types; - } + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-7898.php'; + } - public function dataInferPrivatePropertyTypeFromConstructor(): array - { - return [ - [ - 'int', - '$this->intProp', - ], - [ - 'string', - '$this->stringProp', - ], - [ - 'InferPrivatePropertyTypeFromConstructor\Bar|InferPrivatePropertyTypeFromConstructor\Foo', - '$this->unionProp', - ], - [ - 'stdClass', - '$this->stdClassProp', - ], - [ - 'stdClass', - '$this->unrelatedDocComment', - ], - [ - 'mixed', - '$this->explicitMixed', - ], - [ - 'bool', - '$this->bool', - ], - [ - 'array', - '$this->array', - ], - ]; - } + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Functions/data/bug-7823.php'; + } - /** - * @dataProvider dataInferPrivatePropertyTypeFromConstructor - * @param string $description - * @param string $expression - */ - public function testInferPrivatePropertyTypeFromConstructor( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/infer-private-property-type-from-constructor.php', - $description, - $expression - ); - } + yield __DIR__ . '/../Analyser/data/is-resource-specified.php'; - public function dataPropertyNativeTypes(): array - { - return [ - [ - 'string', - '$this->stringProp', - ], - [ - 'PropertyNativeTypes\Foo', - '$this->selfProp', - ], - [ - 'array', - '$this->integersProp', - ], - ]; - } + 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'; - /** - * @dataProvider dataPropertyNativeTypes - * @param string $description - * @param string $expression - */ - public function testPropertyNativeTypes( - string $description, - string $expression - ): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID >= 80000) { + yield __DIR__ . '/../Rules/Comparison/data/bug-8169.php'; } - $this->assertTypes( - __DIR__ . '/data/property-native-types.php', - $description, - $expression - ); - } - - public function dataArrowFunctions(): array - { - return [ - [ - 'Closure(string): int', - '$x', - ], - [ - 'int', - '$x()', - ], - ]; - } - - /** - * @dataProvider dataArrowFunctions - * @param string $description - * @param string $expression - */ - public function testArrowFunctions( - string $description, - string $expression - ): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + 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 >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-8485.php'; } - $this->assertTypes( - __DIR__ . '/data/arrow-functions.php', - $description, - $expression - ); - } - - public function dataArrowFunctionsInside(): array - { - return [ - [ - 'int', - '$i', - ], - [ - 'string', - '$s', - ], - [ - '*ERROR*', - '$t', - ], - ]; - } - /** - * @dataProvider dataArrowFunctionsInside - * @param string $description - * @param string $expression - */ - public function testArrowFunctionsInside( - string $description, - string $expression - ): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-9007.php'; } - $this->assertTypes( - __DIR__ . '/data/arrow-functions-inside.php', - $description, - $expression - ); - } - public function dataCoalesceAssign(): array - { - return [ - [ - 'string', - '$string ??= 1', - ], - [ - '1|string', - '$nullableString ??= 1', - ], - [ - '\'foo\'', - '$emptyArray[\'foo\'] ??= \'foo\'', - ], - [ - '\'foo\'', - '$arrayWithFoo[\'foo\'] ??= \'bar\'', - ], - [ - '\'bar\'|\'foo\'', - '$arrayWithMaybeFoo[\'foo\'] ??= \'bar\'', - ], - [ - 'array(\'foo\' => \'foo\')', - '$arrayAfterAssignment', - ], - [ - 'array(\'foo\' => \'foo\')', - '$arrayWithFooAfterAssignment', - ], - [ - '\'foo\'', - '$nonexistentVariableAfterAssignment', - ], - [ - '\'bar\'|\'foo\'', - '$maybeNonexistentVariableAfterAssignment', - ], - ]; - } + yield __DIR__ . '/../Rules/DeadCode/data/bug-8620.php'; - /** - * @dataProvider dataCoalesceAssign - * @param string $description - * @param string $expression - */ - public function testCoalesceAssign( - string $description, - string $expression - ): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID >= 80200) { + yield __DIR__ . '/../Rules/Constants/data/bug-8957.php'; } - $this->assertTypes( - __DIR__ . '/data/coalesce-assign.php', - $description, - $expression - ); - } - - public function dataArraySpread(): array - { - return [ - [ - 'array', - '$integersOne', - ], - [ - 'array', - '$integersTwo', - ], - [ - 'array(1, 2, 3, 4, 5, 6, 7)', - '$integersThree', - ], - [ - 'array', - '$integersFour', - ], - [ - 'array', - '$integersFive', - ], - [ - 'array(1, 2, 3, 4, 5, 6, 7)', - '$integersSix', - ], - [ - 'array(1, 2, 3, 4, 5, 6, 7)', - '$integersSeven', - ], - ]; - } - /** - * @dataProvider dataArraySpread - * @param string $description - * @param string $expression - */ - public function testArraySpread( - string $description, - string $expression - ): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); + if (PHP_VERSION_ID >= 80100) { + yield __DIR__ . '/../Rules/Comparison/data/bug-9499.php'; } - $this->assertTypes( - __DIR__ . '/data/array-spread.php', - $description, - $expression - ); - } - - public function dataPhp74FunctionsIn73(): array - { - return [ - [ - '*ERROR*', - 'password_algos()', - ], - ]; - } - /** - * @dataProvider dataPhp74FunctionsIn73 - * @param string $description - * @param string $expression - */ - 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 - ); - } + 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'; - public function dataPhp74FunctionsIn74(): array - { - return [ - [ - 'array', - 'password_algos()', - ], - ]; + yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; + yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; } /** - * @dataProvider dataPhp74FunctionsIn74 - * @param string $description - * @param string $expression + * @return iterable */ - public function testPhp74FunctionsIn74( - string $description, - string $expression - ): void - { - if (PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - $this->assertTypes( - __DIR__ . '/data/die-74.php', - $description, - $expression - ); - } - - public function dataTryCatchScope(): array + public static function dataFile(): iterable { - return [ - [ - 'TryCatchScope\Foo', - '$resource', - "'first'", - ], - [ - 'TryCatchScope\Foo|null', - '$resource', - "'second'", - ], - [ - 'TryCatchScope\Foo|null', - '$resource', - "'third'", - ], - ]; - } + $base = dirname(__DIR__, 3) . '/'; + $baseLength = strlen($base); - /** - * @dataProvider dataTryCatchScope - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression - */ - public function testTryCatchScope( - string $description, - string $expression, - string $evaluatedPointExpression - ): void - { - foreach ([true, false] as $polluteCatchScopeWithTryAssignments) { - $this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments; + $fileHelper = new FileHelper($base); + foreach (self::findTestFiles() as $file) { + $file = $fileHelper->normalizePath($file); - try { - $this->assertTypes( - __DIR__ . '/data/try-catch-scope.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression, - [], - false - ); - } catch (\PHPUnit\Framework\ExpectationFailedException $e) { - throw new \PHPUnit\Framework\ExpectationFailedException( - sprintf( - '%s (polluteCatchScopeWithTryAssignments: %s)', - $e->getMessage(), - $polluteCatchScopeWithTryAssignments ? 'true' : 'false' - ), - $e->getComparisonFailure() - ); + $testName = $file; + if (str_starts_with($file, $base)) { + $testName = substr($file, $baseLength); } + + yield $testName => [$file]; } } /** - * @param string $file - * @param string $description - * @param string $expression - * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions - * @param string $evaluatedPointExpression - * @param string[] $dynamicConstantNames - * @param bool $useCache + * @dataProvider dataFile */ - private function assertTypes( - string $file, - string $description, - string $expression, - array $dynamicMethodReturnTypeExtensions = [], - array $dynamicStaticMethodReturnTypeExtensions = [], - array $methodTypeSpecifyingExtensions = [], - array $staticMethodTypeSpecifyingExtensions = [], - string $evaluatedPointExpression = 'die', - array $dynamicConstantNames = [], - bool $useCache = true - ): void + public function testFile(string $file): void { - $assertType = function (Scope $scope) use ($expression, $description, $evaluatedPointExpression): void { - /** @var \PhpParser\Node\Stmt\Expression $expressionNode */ - $expressionNode = $this->getParser()->parseString(sprintf('getType($expressionNode->expr); - $this->assertTypeDescribe( - $description, - $type, - sprintf('%s at %s', $expression, $evaluatedPointExpression) - ); - }; - if ($useCache && isset(self::$assertTypesCache[$file][$evaluatedPointExpression])) { - $assertType(self::$assertTypesCache[$file][$evaluatedPointExpression]); - return; - } - $this->processFile( - $file, - static function (\PhpParser\Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { - if ($node instanceof VirtualNode) { - return; - } - $printer = new \PhpParser\PrettyPrinter\Standard(); - $printedNode = $printer->prettyPrint([$node]); - if ($printedNode !== $evaluatedPointExpression) { - return; - } + $asserts = $this->gatherAssertTypes($file); + $this->assertNotCount(0, $asserts, sprintf('File %s has no asserts.', $file)); + $failures = []; - self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; + foreach ($asserts as $args) { + $assertType = array_shift($args); + $file = array_shift($args); - $assertType($scope); - }, - $dynamicMethodReturnTypeExtensions, - $dynamicStaticMethodReturnTypeExtensions, - $methodTypeSpecifyingExtensions, - $staticMethodTypeSpecifyingExtensions, - $dynamicConstantNames - ); - } + if ($assertType === 'type') { + $expected = $args[0]; + $actual = $args[1]; - /** - * @param string $file - * @param \Closure $callback - * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions - * @param string[] $dynamicConstantNames - */ - private function processFile( - string $file, - \Closure $callback, - array $dynamicMethodReturnTypeExtensions = [], - array $dynamicStaticMethodReturnTypeExtensions = [], - array $methodTypeSpecifyingExtensions = [], - array $staticMethodTypeSpecifyingExtensions = [], - array $dynamicConstantNames = [] - ): void - { - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); + 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]; - $printer = new \PhpParser\PrettyPrinter\Standard(); - $broker = $this->createBroker($dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions); - Broker::registerInstance($broker); - $typeSpecifier = $this->createTypeSpecifier($printer, $broker, $methodTypeSpecifyingExtensions, $staticMethodTypeSpecifyingExtensions); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $fileTypeMapper = new FileTypeMapper(new DirectReflectionProviderProvider($broker), $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), new AnonymousClassNameHelper($fileHelper, new SimpleRelativePathHelper($currentWorkingDirectory))); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); - $resolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), - $this->getParser(), - $fileTypeMapper, - $phpDocInheritanceResolver, - $fileHelper, - $typeSpecifier, - true, - $this->polluteCatchScopeWithTryAssignments, - true, - [ - \EarlyTermination\Foo::class => [ - 'doFoo', - 'doBar', - ], - ], - ['baz'] - ); - $resolver->setAnalysedFiles(array_map(static function (string $file) use ($fileHelper): string { - return $fileHelper->normalizePath($file); - }, [ - $file, - __DIR__ . '/data/methodPhpDocs-trait-defined.php', - __DIR__ . '/data/anonymous-class-name-in-trait-trait.php', - ])); + 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()); + } + } + } - $scopeFactory = $this->createScopeFactory($broker, $typeSpecifier); - if (count($dynamicConstantNames) > 0) { - $reflectionProperty = new \ReflectionProperty(DirectScopeFactory::class, 'dynamicConstantNames'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($scopeFactory, $dynamicConstantNames); + if ($failures === []) { + return; } - $scope = $scopeFactory->create(ScopeContext::create($file)); - $resolver->processNodes( - $this->getParser()->parseFile($file), - $scope, - $callback - ); + self::fail(sprintf("Failed assertions in %s:\n\n%s", $file, implode("\n", $failures))); } - public function dataDeclareStrictTypes(): array + public static function getAdditionalConfigFiles(): array { return [ - [ - __DIR__ . '/data/declareWeakTypes.php', - false, - ], - [ - __DIR__ . '/data/noDeclare.php', - false, - ], - [ - __DIR__ . '/data/declareStrictTypes.php', - true, - ], + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/typeAliases.neon', ]; } - /** - * @dataProvider dataDeclareStrictTypes - * @param string $file - * @param bool $result - */ - public function testDeclareStrictTypes(string $file, bool $result): void - { - $this->processFile($file, function (\PhpParser\Node $node, Scope $scope) use ($result): void { - if (!($node instanceof Exit_)) { - return; - } - - $this->assertSame($result, $scope->isDeclareStrictTypes()); - }); - } - - public function testEarlyTermination(): void - { - $this->processFile(__DIR__ . '/data/early-termination.php', function (\PhpParser\Node $node, Scope $scope): void { - if (!($node instanceof Exit_)) { - return; - } - - $this->assertTrue($scope->hasVariableType('something')->yes()); - $this->assertTrue($scope->hasVariableType('var')->yes()); - $this->assertTrue($scope->hasVariableType('foo')->no()); - }); - } - - private function assertTypeDescribe( - string $expectedDescription, - Type $actualType, - string $label = '' - ): void - { - $actualDescription = $actualType->describe(VerbosityLevel::precise()); - $this->assertSame( - $expectedDescription, - $actualDescription, - $label - ); - } - } 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 4c1f9c663c..4612ef1596 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -4,17 +4,23 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Name\FullyQualified; -use PHPStan\Testing\TestCase; +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; -class ScopeTest extends TestCase +/** + * @covers \PHPStan\Analyser\MutatingScope + */ +class ScopeTest extends PHPStanTestCase { public function dataGeneralize(): array @@ -28,12 +34,12 @@ public function dataGeneralize(): array [ new ConstantStringType('a'), new ConstantStringType('b'), - 'string', + 'literal-string&lowercase-string&non-falsy-string', ], [ new ConstantIntegerType(0), new ConstantIntegerType(1), - 'int', + 'int<0, max>', ], [ new UnionType([ @@ -45,7 +51,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'int', + 'int<0, max>', ], [ new UnionType([ @@ -72,7 +78,7 @@ public function dataGeneralize(): array new ConstantIntegerType(2), new ConstantStringType('foo'), ]), - '\'foo\'|int', + '\'foo\'|int<0, max>', ], [ new ConstantBooleanType(false), @@ -93,7 +99,7 @@ public function dataGeneralize(): array [ new ObjectType('Foo'), new ConstantBooleanType(false), - 'Foo', + 'Foo|false', ], [ new ConstantArrayType([ @@ -106,7 +112,7 @@ public function dataGeneralize(): array ], [ new ConstantIntegerType(1), ]), - 'array(\'a\' => 1)', + 'array{a: 1}', ], [ new ConstantArrayType([ @@ -123,7 +129,7 @@ public function dataGeneralize(): array new ConstantIntegerType(2), new ConstantIntegerType(1), ]), - 'array(\'a\' => int, \'b\' => 1)', + 'array{a: int<1, max>, b: 1}', ], [ new ConstantArrayType([ @@ -138,7 +144,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'array', + 'non-empty-array', ], [ new ConstantArrayType([ @@ -153,23 +159,86 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'array', + 'non-empty-array>', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]), + new UnionType([ + new ConstantIntegerType(-1), + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]), + 'int', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ]), + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + '0|1|2', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ]), + '0|1|2', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(1, 17), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(-1, 15), + 'int', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(1, null), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(null, 15), + 'int', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(0, null), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(null, 16), + 'int', ], ]; } /** * @dataProvider dataGeneralize - * @param Type $a - * @param Type $b - * @param string $expectedTypeDescription */ public function testGeneralize(Type $a, Type $b, string $expectedTypeDescription): void { /** @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())); } @@ -181,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', $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 2664cbb3d3..da7d60bd3d 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -4,9 +4,15 @@ 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; use PHPStan\Type\StringType; +use function sprintf; -class StatementResultTest extends \PHPStan\Testing\TestCase +class StatementResultTest extends PHPStanTestCase { public function dataIsAlwaysTerminating(): array @@ -141,7 +147,7 @@ public function dataIsAlwaysTerminating(): array true, ], [ - 'try { return; } catch (Exception $e) { }', + 'try { maybeThrow(); return; } catch (Exception $e) { }', false, ], [ @@ -153,7 +159,7 @@ public function dataIsAlwaysTerminating(): array true, ], [ - 'try { break; } catch (Exception $e) { break; } catch (OtherException $e) { }', + 'try { maybeThrow(); break; } catch (Exception $e) { break; } catch (OtherException $e) { }', false, ], [ @@ -168,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, @@ -221,12 +359,12 @@ public function dataIsAlwaysTerminating(): array true, ], [ - 'while ($bool) { return; }', + 'while ($cond) { return; }', false, ], [ 'for ($i = 0; $i < 10; $i++) { return; }', - false, // will be true with range types + true, ], [ 'for ($i = 0; $i < 0; $i++) { return; }', @@ -245,7 +383,7 @@ public function dataIsAlwaysTerminating(): array true, ], [ - 'do { return; } while ($maybe);', + 'do { return; } while ($cond);', true, ], [ @@ -265,7 +403,7 @@ public function dataIsAlwaysTerminating(): array false, ], [ - 'switch ($i) { case 0: return 1; case 1: case 2: default: }', + 'switch ($x) { case 0: return 1; case 1: case 2: default: }', false, ], [ @@ -321,15 +459,15 @@ public function dataIsAlwaysTerminating(): array true, ], [ - 'while ($string !== null) { $string = null; try { return true; } catch (\Exception $e) { doFoo(); } }', + 'while ($string !== null) { $string = null; try { maybeThrow(); return true; } catch (\Exception $e) { doFoo(); } }', false, ], [ - 'while ($string !== null) { $string = null; try { return true; } catch (\Exception $e) { doFoo(); } }', + 'while ($string !== null) { $string = null; try { maybeThrow(); return true; } catch (\Exception $e) { doFoo(); } }', false, ], [ - 'try { return true; } catch (\Exception $e) { doFoo(); }', + 'try { maybeThrow(); return true; } catch (\Exception $e) { doFoo(); }', false, ], [ @@ -373,16 +511,14 @@ public function dataIsAlwaysTerminating(): array /** * @dataProvider dataIsAlwaysTerminating - * @param string $code - * @param bool $expectedIsAlwaysTerminating */ public function testIsAlwaysTerminating( string $code, - bool $expectedIsAlwaysTerminating + bool $expectedIsAlwaysTerminating, ): void { /** @var Parser $parser */ - $parser = self::getContainer()->getByType(Parser::class); + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); /** @var Stmt[] $stmts */ $stmts = $parser->parseString(sprintf('getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('test.php')) - ->assignVariable('string', new StringType()); + ->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/TypeSpecifierContextTest.php b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php index ac18184fd0..c6e083ed5d 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierContextTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php @@ -2,7 +2,10 @@ namespace PHPStan\Analyser; -class TypeSpecifierContextTest extends \PHPStan\Testing\TestCase +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; + +class TypeSpecifierContextTest extends PHPStanTestCase { public function dataContext(): array @@ -33,7 +36,6 @@ public function dataContext(): array /** * @dataProvider dataContext - * @param \PHPStan\Analyser\TypeSpecifierContext $context * @param bool[] $results */ public function testContext(TypeSpecifierContext $context, array $results): void @@ -69,7 +71,6 @@ public function dataNegate(): array /** * @dataProvider dataNegate - * @param \PHPStan\Analyser\TypeSpecifierContext $context * @param bool[] $results */ public function testNegate(TypeSpecifierContext $context, array $results): void @@ -83,7 +84,7 @@ public function testNegate(TypeSpecifierContext $context, array $results): void public function testNegateNull(): void { - $this->expectException(\PHPStan\ShouldNotHappenException::class); + $this->expectException(ShouldNotHappenException::class); TypeSpecifierContext::createNull()->negate(); } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 5b3f773881..0957b36f22 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotIdentical; use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Variable; @@ -16,47 +17,70 @@ use PhpParser\Node\Scalar\LNumber; 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; +use PHPStan\Type\FloatType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function implode; +use function sprintf; +use const PHP_INT_MAX; +use const PHP_INT_MIN; +use const PHP_VERSION_ID; -class TypeSpecifierTest extends \PHPStan\Testing\TestCase +class TypeSpecifierTest extends PHPStanTestCase { - /** @var \PhpParser\PrettyPrinter\Standard() */ - private $printer; + private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array{}|false|null'; + private const TRUTHY_TYPE_DESCRIPTION = 'mixed~(' . self::FALSEY_TYPE_DESCRIPTION . ')'; + private const SURE_NOT_FALSEY = '~' . self::FALSEY_TYPE_DESCRIPTION; + private const SURE_NOT_TRUTHY = '~' . self::TRUTHY_TYPE_DESCRIPTION; - /** @var \PHPStan\Analyser\TypeSpecifier */ - private $typeSpecifier; + /** @var Standard () */ + private Standard $printer; - /** @var Scope */ - private $scope; + private TypeSpecifier $typeSpecifier; + + private Scope $scope; protected function setUp(): void { - $broker = $this->createBroker(); - $this->printer = new \PhpParser\PrettyPrinter\Standard(); - $this->typeSpecifier = $this->createTypeSpecifier($this->printer, $broker); - $this->scope = $this->createScopeFactory($broker, $this->typeSpecifier)->create(ScopeContext::create('')); - $this->scope = $this->scope->enterClass($broker->getClass('DateTime')); - $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()); + $reflectionProvider = $this->createReflectionProvider(); + $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'), 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()); } /** * @dataProvider dataCondition - * @param Expr $expr * @param mixed[] $expectedPositiveResult * @param mixed[] $expectedNegatedResult */ @@ -71,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'], @@ -81,8 +137,8 @@ public function dataCondition(): array ], [ $this->createFunctionCall('is_numeric'), - ['$foo' => 'float|int|string'], - ['$foo' => '~float|int'], + ['$foo' => 'float|int|numeric-string'], + ['$foo' => '~float|int|numeric-string'], ], [ $this->createFunctionCall('is_scalar'), @@ -92,7 +148,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => 'int'], [], @@ -100,7 +156,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => '~int'], @@ -108,7 +164,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\LogicalAnd( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => 'int'], [], @@ -116,7 +172,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\LogicalOr( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => '~int'], @@ -130,7 +186,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new Expr\BooleanNot($this->createFunctionCall('is_int')), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => '~int'], [], @@ -138,7 +194,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new Expr\BooleanNot($this->createFunctionCall('is_int')), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => 'int'], @@ -161,7 +217,7 @@ public function dataCondition(): array [ new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), ['$foo' => 'object'], [], @@ -171,89 +227,109 @@ public function dataCondition(): array new FuncCall(new Name('get_class'), [ new Arg(new Variable('foo')), ]), - new String_('Foo') + new String_('Foo'), ), - ['$foo' => 'Foo'], - ['$foo' => '~Foo'], + ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], + ['get_class($foo)' => '~\'Foo\''], ], [ new Equal( new String_('Foo'), new FuncCall(new Name('get_class'), [ new Arg(new Variable('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'], - ['$foo' => '~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( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') - ) + new Variable('className'), + ), ), [], ['$foo' => 'object'], ], [ new Variable('foo'), - ['$foo' => '~0|0.0|\'\'|array()|false|null'], - ['$foo' => '~object|true|nonEmpty'], + ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => self::SURE_NOT_TRUTHY], ], [ new Expr\BinaryOp\BooleanAnd( new Variable('foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), - ['$foo' => '~0|0.0|\'\'|array()|false|null'], + ['$foo' => self::SURE_NOT_FALSEY], [], ], [ new Expr\BinaryOp\BooleanOr( new Variable('foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], - ['$foo' => '~object|true|nonEmpty'], + ['$foo' => self::SURE_NOT_TRUTHY], ], [ new Expr\BooleanNot(new Variable('bar')), - ['$bar' => '~object|true|nonEmpty'], - ['$bar' => '~0|0.0|\'\'|array()|false|null'], + ['$bar' => self::SURE_NOT_TRUTHY], + ['$bar' => self::SURE_NOT_FALSEY], ], [ new PropertyFetch(new Variable('this'), 'foo'), - ['$this->foo' => '~0|0.0|\'\'|array()|false|null'], - ['$this->foo' => '~object|true|nonEmpty'], + ['$this->foo' => self::SURE_NOT_FALSEY], + ['$this->foo' => self::SURE_NOT_TRUTHY], ], [ new Expr\BinaryOp\BooleanAnd( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), - ['$this->foo' => '~0|0.0|\'\'|array()|false|null'], + ['$this->foo' => self::SURE_NOT_FALSEY], [], ], [ new Expr\BinaryOp\BooleanOr( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], - ['$this->foo' => '~object|true|nonEmpty'], + ['$this->foo' => self::SURE_NOT_TRUTHY], ], [ new Expr\BooleanNot(new PropertyFetch(new Variable('this'), 'foo')), - ['$this->foo' => '~object|true|nonEmpty'], - ['$this->foo' => '~0|0.0|\'\'|array()|false|null'], + ['$this->foo' => self::SURE_NOT_TRUTHY], + ['$this->foo' => self::SURE_NOT_FALSEY], ], [ new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int'), - $this->createFunctionCall('is_string') + $this->createFunctionCall('is_string'), ), ['$foo' => 'int|string'], ['$foo' => '~int|string'], @@ -263,8 +339,8 @@ public function dataCondition(): array $this->createFunctionCall('is_int'), new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_string'), - $this->createFunctionCall('is_bool') - ) + $this->createFunctionCall('is_bool'), + ), ), ['$foo' => 'bool|int|string'], ['$foo' => '~bool|int|string'], @@ -272,7 +348,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'bar') + $this->createFunctionCall('is_string', 'bar'), ), [], ['$foo' => '~int', '$bar' => '~string'], @@ -281,9 +357,9 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'foo') + $this->createFunctionCall('is_string', 'foo'), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => 'int|string'], [], @@ -292,20 +368,20 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'foo') + $this->createFunctionCall('is_string', 'foo'), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], - ['$foo' => '~*NEVER*'], + ['$foo' => 'mixed'], ], [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'bar') + $this->createFunctionCall('is_string', 'bar'), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], [], @@ -314,9 +390,9 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( new Expr\BooleanNot($this->createFunctionCall('is_int', 'foo')), - new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')) + new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => 'int|string'], @@ -325,42 +401,50 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\BooleanOr( new Expr\BooleanNot($this->createFunctionCall('is_int', 'foo')), - new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')) + new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), - ['$foo' => '~*NEVER*'], + ['$foo' => 'mixed'], [], ], [ new Identical( new Variable('foo'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), - ['$foo' => 'true & ~0|0.0|\'\'|array()|false|null'], + ['$foo' => 'true & ~' . self::FALSEY_TYPE_DESCRIPTION], ['$foo' => '~true'], ], [ new Identical( new Variable('foo'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), - ['$foo' => 'false & ~object|true|nonEmpty'], + ['$foo' => 'false & ~' . self::TRUTHY_TYPE_DESCRIPTION], ['$foo' => '~false'], ], [ new Identical( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), ['is_int($foo)' => 'true', '$foo' => 'int'], ['is_int($foo)' => '~true', '$foo' => '~int'], ], + [ + 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'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['is_int($foo)' => 'false', '$foo' => '~int'], ['$foo' => 'int', 'is_int($foo)' => '~false'], @@ -368,7 +452,7 @@ public function dataCondition(): array [ new Equal( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), ['$foo' => 'int'], ['$foo' => '~int'], @@ -376,7 +460,7 @@ public function dataCondition(): array [ new Equal( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['$foo' => '~int'], ['$foo' => 'int'], @@ -384,25 +468,25 @@ public function dataCondition(): array [ new Equal( new Variable('foo'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), - ['$foo' => '~object|true|nonEmpty'], - ['$foo' => '~0|0.0|\'\'|array()|false|null'], + ['$foo' => self::SURE_NOT_TRUTHY], + ['$foo' => self::SURE_NOT_FALSEY], ], [ new Equal( new Variable('foo'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), - ['$foo' => '~object|true|nonEmpty'], - ['$foo' => '~0|0.0|\'\'|array()|false|null'], + ['$foo' => '0|0.0|\'\'|array{}|false|null'], + ['$foo' => '~0|0.0|\'\'|array{}|false|null'], ], [ new Expr\BinaryOp\Identical( new Variable('foo'), - new Variable('bar') + new Variable('bar'), ), - ['$foo' => 'Bar', '$bar' => 'Bar'], + ['$foo' => 'Bar', '$bar' => 'mixed'], // could be '$bar' => 'Bar' [], ], [ @@ -426,11 +510,27 @@ public function dataCondition(): array new Arg(new Variable('foo')), new Arg(new Expr\ClassConstFetch( new Name('static'), - 'class' + 'class', )), ]), ['$foo' => 'static(DateTime)'], - ['$foo' => '~static(DateTime)'], + [], + ], + [ + new FuncCall(new Name('is_a'), [ + new Arg(new Variable('foo')), + new Arg(new Variable('classString')), + ]), + ['$foo' => 'object'], + [], + ], + [ + new FuncCall(new Name('is_a'), [ + new Arg(new Variable('foo')), + new Arg(new Variable('genericClassString')), + ]), + ['$foo' => 'Bar'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -438,8 +538,8 @@ public function dataCondition(): array new Arg(new String_('Foo')), new Arg(new Expr\ConstFetch(new Name('true'))), ]), - ['$foo' => 'Foo|string'], - ['$foo' => '~Foo'], + ['$foo' => 'class-string|Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -447,7 +547,7 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Expr\ConstFetch(new Name('true'))), ]), - ['$foo' => 'object|string'], + ['$foo' => 'class-string|object'], [], ], [ @@ -456,8 +556,8 @@ public function dataCondition(): array new Arg(new String_('Foo')), new Arg(new Variable('unknown')), ]), - ['$foo' => 'Foo|string'], - ['$foo' => '~Foo'], + ['$foo' => 'class-string|Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -465,32 +565,43 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Variable('unknown')), ]), - ['$foo' => 'object|string'], + ['$foo' => 'class-string|object'], [], ], [ new Expr\Assign( new Variable('foo'), - new Variable('stringOrNull') + new Variable('stringOrNull'), ), - ['$foo' => '~0|0.0|\'\'|array()|false|null'], - ['$foo' => '~object|true|nonEmpty'], + ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => self::SURE_NOT_TRUTHY], ], [ new Expr\Assign( new Variable('foo'), - new Variable('stringOrFalse') + new Variable('stringOrFalse'), ), - ['$foo' => '~0|0.0|\'\'|array()|false|null'], - ['$foo' => '~object|true|nonEmpty'], + ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => self::SURE_NOT_TRUTHY], ], [ new Expr\Assign( new Variable('foo'), - new Variable('bar') + new Variable('bar'), ), - ['$foo' => '~0|0.0|\'\'|array()|false|null'], - ['$foo' => '~object|true|nonEmpty'], + ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => self::SURE_NOT_TRUTHY], + ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + ]), + [ + '$stringOrNull' => '~null', + ], + [ + '$stringOrNull' => 'null', + ], ], [ new Expr\Isset_([ @@ -501,46 +612,56 @@ public function dataCondition(): array '$stringOrNull' => '~null', '$barOrNull' => '~null', ], + [], + ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + new Variable('barOrNull'), + new Variable('fooOrNull'), + ]), [ - 'isset($stringOrNull, $barOrNull)' => '~object|true|nonEmpty', + '$stringOrNull' => '~null', + '$barOrNull' => '~null', + '$fooOrNull' => '~null', ], + [], ], [ new Expr\BooleanNot(new Expr\Empty_(new Variable('stringOrNull'))), [ - '$stringOrNull' => '~false|null', + '$stringOrNull' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ - 'empty($stringOrNull)' => '~0|0.0|\'\'|array()|false|null', + '$stringOrNull' => '\'\'|\'0\'|null', ], ], [ new Expr\BinaryOp\Identical( new Variable('foo'), - new LNumber(123) + new LNumber(123), ), [ '$foo' => '123', - 123 => '123', ], ['$foo' => '~123'], ], [ new Expr\Empty_(new Variable('array')), [ - '$array' => '~nonEmpty', + '$array' => 'array{}|null', ], [ - '$array' => 'nonEmpty & ~false|null', + '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], ], [ new BooleanNot(new Expr\Empty_(new Variable('array'))), [ - '$array' => 'nonEmpty & ~false|null', + '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ - '$array' => '~nonEmpty', + '$array' => 'array{}|null', ], ], [ @@ -548,10 +669,10 @@ public function dataCondition(): array new Arg(new Variable('array')), ]), [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], ], [ @@ -559,37 +680,59 @@ public function dataCondition(): array new Arg(new Variable('array')), ])), [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', + ], + [ + '$array' => 'non-empty-array', + ], + ], + [ + new FuncCall(new Name('sizeof'), [ + new Arg(new Variable('array')), + ]), + [ + '$array' => 'non-empty-array', + ], + [ + '$array' => '~non-empty-array', + ], + ], + [ + new BooleanNot(new FuncCall(new Name('sizeof'), [ + new Arg(new Variable('array')), + ])), + [ + '$array' => '~non-empty-array', ], [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], ], [ new Variable('foo'), [ - '$foo' => '~0|0.0|\'\'|array()|false|null', + '$foo' => self::SURE_NOT_FALSEY, ], [ - '$foo' => '~object|true|nonEmpty', + '$foo' => self::SURE_NOT_TRUTHY, ], ], [ new Variable('array'), [ - '$array' => '~0|0.0|\'\'|array()|false|null', + '$array' => self::SURE_NOT_FALSEY, ], [ - '$array' => '~object|true|nonEmpty', + '$array' => self::SURE_NOT_TRUTHY, ], ], [ new Equal( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), - new LNumber(1) + new LNumber(1), ), ['$foo' => 'object'], [], @@ -598,9 +741,9 @@ public function dataCondition(): array new Equal( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), - new LNumber(0) + new LNumber(0), ), [], [ @@ -611,33 +754,29 @@ public function dataCondition(): array new Expr\Isset_( [ new PropertyFetch(new Variable('foo'), new Identifier('bar')), - ] + ], ), [ '$foo' => 'object&hasProperty(bar) & ~null', '$foo->bar' => '~null', ], - [ - 'isset($foo->bar)' => '~object|true|nonEmpty', - ], + [], ], [ new Expr\Isset_( [ new Expr\StaticPropertyFetch(new Name('Foo'), new VarLikeIdentifier('bar')), - ] + ], ), [ 'Foo::$bar' => '~null', ], - [ - 'isset(Foo::$bar)' => '~object|true|nonEmpty', - ], + [], ], [ new Identical( new Variable('barOrNull'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$barOrNull' => 'null', @@ -650,21 +789,23 @@ public function dataCondition(): array new Identical( new Expr\Assign( new Variable('notNullBar'), - new Variable('barOrNull') + new Variable('barOrNull'), ), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], ], [ new NotIdentical( new Variable('barOrNull'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$barOrNull' => '~null', @@ -676,47 +817,95 @@ public function dataCondition(): array [ new Expr\BinaryOp\Smaller( new Variable('n'), - new LNumber(3) + new LNumber(3), + ), + [ + '$n' => 'mixed~(int<3, max>|true)', + ], + [ + '$n' => 'mixed~(0.0|int|false|null)', + ], + ], + [ + new Expr\BinaryOp\Smaller( + new Variable('n'), + new LNumber(PHP_INT_MIN), + ), + [ + '$n' => 'mixed~(int<' . PHP_INT_MIN . ', max>|true)', + ], + [ + '$n' => 'mixed~(0.0|false|null)', + ], + ], + [ + new Expr\BinaryOp\Greater( + new Variable('n'), + new LNumber(PHP_INT_MAX), ), [ - '$n' => '~int<3, max>', + '$n' => 'mixed~(0.0|bool|int|null)', ], [ - '$n' => '~int', + '$n' => 'mixed', + ], + ], + [ + new Expr\BinaryOp\SmallerOrEqual( + new Variable('n'), + new LNumber(PHP_INT_MIN), + ), + [ + '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', + ], + [ + '$n' => 'mixed~(0.0|bool|int|null)', + ], + ], + [ + new Expr\BinaryOp\GreaterOrEqual( + new Variable('n'), + new LNumber(PHP_INT_MAX), + ), + [ + '$n' => 'mixed~(0.0|int|false|null)', + ], + [ + '$n' => 'mixed~(int<' . PHP_INT_MAX . ', max>|true)', ], ], [ new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\GreaterOrEqual( new Variable('n'), - new LNumber(3) + new LNumber(3), ), new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(5) - ) + new LNumber(5), + ), ), [ - '$n' => '~int<6, max>|int', + '$n' => 'mixed~(0.0|int|int<6, max>|false|null)', ], [ - '$n' => '~int<3, 5>', + '$n' => 'mixed~(int<3, 5>|true)', ], ], [ new Expr\BinaryOp\BooleanAnd( new Expr\Assign( new Variable('foo'), - new LNumber(1) + new LNumber(1), ), new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(5) - ) + new LNumber(5), + ), ), [ - '$foo' => '~0|0.0|\'\'|array()|false|null', - '$n' => '~int<6, max>', + '$n' => 'mixed~int<6, max>', + '$foo' => self::SURE_NOT_FALSEY, ], [], ], @@ -724,24 +913,26 @@ public function dataCondition(): array new NotIdentical( new Expr\Assign( new Variable('notNullBar'), - new Variable('barOrNull') + new Variable('barOrNull'), ), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], ], [ new Identical( new Variable('barOrFalse'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ - '$barOrFalse' => 'false & ~object|true|nonEmpty', + '$barOrFalse' => 'false & ' . self::SURE_NOT_TRUTHY, ], [ '$barOrFalse' => '~false', @@ -751,57 +942,84 @@ public function dataCondition(): array new Identical( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ - '$notFalseBar' => 'false & ~object|true|nonEmpty', + '$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', ], ], [ new NotIdentical( new Variable('barOrFalse'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$barOrFalse' => '~false', ], [ - '$barOrFalse' => 'false & ~object|true|nonEmpty', + '$barOrFalse' => 'false & ' . self::SURE_NOT_TRUTHY, ], ], [ new NotIdentical( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', ], [ - '$notFalseBar' => 'false & ~object|true|nonEmpty', + '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$barOrFalse' => 'false', ], ], [ new Expr\Instanceof_( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Name('Bar') + new Name('Bar'), ), [ '$notFalseBar' => 'Bar', + '$barOrFalse' => 'Bar', ], [ '$notFalseBar' => '~Bar', + '$barOrFalse' => '~Bar', ], ], [ @@ -813,10 +1031,10 @@ public function dataCondition(): array new FuncCall(new Name('array_key_exists'), [ new Arg(new String_('bar')), new Arg(new Variable('array')), - ]) + ]), ), [ - '$array' => 'array', + '$array' => 'non-empty-array', ], [ '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', @@ -831,13 +1049,13 @@ public function dataCondition(): array new FuncCall(new Name('array_key_exists'), [ new Arg(new String_('bar')), new Arg(new Variable('array')), - ]) + ]), )), [ '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', ], [ - '$array' => 'array', + '$array' => 'non-empty-array', ], ], [ @@ -846,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\')', @@ -862,6 +1128,15 @@ public function dataCondition(): array ], [], ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('object')), + new Arg(new Variable('stringOrNull')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [], + [], + ], [ new FuncCall(new Name('is_subclass_of'), [ new Arg(new Variable('string')), @@ -873,11 +1148,179 @@ public function dataCondition(): array ], [], ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('string')), + new Arg(new Variable('genericClassString')), + ]), + [ + '$string' => 'Bar|class-string', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('object')), + new Arg(new Variable('genericClassString')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$object' => 'Bar', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('string')), + new Arg(new Variable('genericClassString')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$string' => 'Bar', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_string', 'a'), + new NotIdentical(new String_(''), new Variable('a')), + ), + new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), + ), + ['$a' => 'non-empty-string|null'], + ['$a' => 'mixed~non-empty-string & ~null'], + ], + [ + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_string', 'a'), + new Expr\BinaryOp\Greater( + $this->createFunctionCall('strlen', 'a'), + new LNumber(0), + ), + ), + new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), + ), + ['$a' => 'non-empty-string|null'], + ['$a' => 'mixed~non-empty-string & ~null'], + ], + [ + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'a'), + new Expr\BinaryOp\Greater( + $this->createFunctionCall('count', 'a'), + new LNumber(0), + ), + ), + new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), + ), + ['$a' => 'non-empty-array|null'], + ['$a' => 'mixed~non-empty-array & ~null'], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'foo'), + new Identical( + new FuncCall( + new Name('array_filter'), + [new Arg(new Variable('foo')), new Arg(new String_('is_string')), new Arg(new ConstFetch(new Name('ARRAY_FILTER_USE_KEY')))], + ), + new Variable('foo'), + ), + ), + [ + '$foo' => 'array', + 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', // could be 'array' + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'foo'), + new Expr\BinaryOp\GreaterOrEqual( + new FuncCall( + new Name('count'), + [new Arg(new Variable('foo'))], + ), + new LNumber(2), + ), + ), + [ + '$foo' => 'non-empty-array', + 'count($foo)' => 'mixed~(0.0|int|false|null)', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'foo'), + new Identical( + new FuncCall( + new Name('count'), + [new Arg(new Variable('foo'))], + ), + new LNumber(2), + ), + ), + [ + '$foo' => 'non-empty-array', + 'count($foo)' => '2', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_string', 'foo'), + new NotIdentical( + new FuncCall( + new Name('strlen'), + [new Arg(new Variable('foo'))], + ), + new LNumber(0), + ), + ), + [ + '$foo' => "string & ~''", + 'strlen($foo)' => '~0', + ], + [ + '$foo' => 'mixed~non-empty-string', + ], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_numeric', 'int'), + new Expr\BinaryOp\Equal( + new Variable('int'), + new Expr\Cast\Int_(new Variable('int')), + ), + ), + [ + '$int' => 'int', + '(int) $int' => 'int', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_numeric', 'float'), + new Expr\BinaryOp\Equal( + new Variable('float'), + new Expr\Cast\Int_(new Variable('float')), + ), + ), + [ + '$float' => 'float', + '(int) $float' => 'int', + ], + [], + ], ]; } /** - * @param \PHPStan\Analyser\SpecifiedTypes $specifiedTypes * @return mixed[] */ private function toReadableResult(SpecifiedTypes $specifiedTypes): array @@ -900,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/TypeSpecifyingExtension-false.neon b/tests/PHPStan/Analyser/TypeSpecifyingExtension-false.neon new file mode 100644 index 0000000000..094662df08 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtension-false.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension + arguments: + nullContext: false + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension + arguments: + nullContext: false + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtension-null.neon b/tests/PHPStan/Analyser/TypeSpecifyingExtension-null.neon new file mode 100644 index 0000000000..d208833ec3 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtension-null.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension + arguments: + nullContext: null + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension + arguments: + nullContext: null + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtension-true.neon b/tests/PHPStan/Analyser/TypeSpecifyingExtension-true.neon new file mode 100644 index 0000000000..cd413d0751 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtension-true.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension + arguments: + nullContext: true + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension + arguments: + nullContext: true + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php new file mode 100644 index 0000000000..3378922993 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-false.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-false.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-false.php'); + } + + /** + * @dataProvider dataTypeSpecifyingExtensionsFalse + * @param mixed ...$args + */ + public function testTypeSpecifyingExtensionsFalse( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifyingExtension-false.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php new file mode 100644 index 0000000000..bc8276fb59 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-null.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-null.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-null.php'); + } + + /** + * @dataProvider dataTypeSpecifyingExtensionsNull + * @param mixed ...$args + */ + public function testTypeSpecifyingExtensionsNull( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifyingExtension-null.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php new file mode 100644 index 0000000000..7478a93f2a --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-true.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-true.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-true.php'); + } + + /** + * @dataProvider dataTypeSpecifyingExtensionsTrue + * @param mixed ...$args + */ + public function testTypeSpecifyingExtensionsTrue( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifyingExtension-true.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/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/classConstantStub.stub b/tests/PHPStan/Analyser/classConstantStub.stub new file mode 100644 index 0000000000..b88ee8b705 --- /dev/null +++ b/tests/PHPStan/Analyser/classConstantStub.stub @@ -0,0 +1,14 @@ +foo->foo(); } + public function setFoo(self $foo): void + { + $this->foo = $foo; + } + } diff --git a/tests/PHPStan/Analyser/data/Foo-callable.php b/tests/PHPStan/Analyser/data/Foo-callable.php index 506442d7fc..99ec3be957 100644 --- a/tests/PHPStan/Analyser/data/Foo-callable.php +++ b/tests/PHPStan/Analyser/data/Foo-callable.php @@ -7,7 +7,7 @@ class Foo { /** - * @param Foo|callable $xxx + * @param Foo|(callable(): mixed) $xxx */ public function abc($xxx): void { diff --git a/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..eb02e6e436 --- /dev/null +++ b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000000..dd72c4525e --- /dev/null +++ b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php @@ -0,0 +1,301 @@ +getName(), ['getByPrimary'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + $args = $methodCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + $arg = $args[0]->value; + if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + if (!($arg->class instanceof \PhpParser\Node\Name)) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + return new ObjectType((string) $arg->class); + } + +} + +class OffsetGetDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\ComponentContainer::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'offsetGet'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $args = $methodCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + $argType = $scope->getType($args[0]->value); + if (!$argType instanceof ConstantStringType) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + return new ObjectType($argType->getValue()); + } + +} + +class CreateManagerForEntityDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\EntityManager::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), ['createManagerForEntity'], true); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + $args = $methodCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + $arg = $args[0]->value; + if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + if (!($arg->class instanceof \PhpParser\Node\Name)) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + return new ObjectType((string) $arg->class); + } + +} + +class ConstructDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\Foo::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + return new ObjectWithoutClassType(); + } + +} + +class ConstructWithoutConstructor implements DynamicStaticMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\FooWithoutConstructor::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + return new ObjectWithoutClassType(); + } + +} + +class GetSelfDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnCompoundTypes\Collection::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getSelf'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return new ObjectType(\DynamicMethodReturnCompoundTypes\Collection::class); + } + +} + +class FooGetSelf implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnCompoundTypes\Foo::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getSelf'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return new ObjectType(\DynamicMethodReturnCompoundTypes\Foo::class); + } + +} + + +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 @@ + true, 'value' => '123']; diff --git a/tests/PHPStan/Analyser/data/array-functions.php b/tests/PHPStan/Analyser/data/array-functions.php index 8542ded468..15d7252487 100644 --- a/tests/PHPStan/Analyser/data/array-functions.php +++ b/tests/PHPStan/Analyser/data/array-functions.php @@ -36,10 +36,17 @@ }, 1); -$reversedIntegers = array_reverse($integers); - $filledIntegers = array_fill(0, 5, 1); +$emptyFilled = array_fill(3, 0, 'banana'); $filledIntegersWithKeys = array_fill_keys([0], 1); +/** @var negative-int $negInt */ +$filledAlwaysFalse = array_fill(0, $negInt, 1); +/** @var positive-int $posInt */ +$filledNonEmptyArray = array_fill(0, $posInt, 'foo'); +$filledNegativeConstAlwaysFalse = array_fill(0, -5, 1); +/** @var int<-3, 5> $maybeNegRange */ +$filledByMaybeNegativeRange = array_fill(0, $maybeNegRange, 1); +$filledByPositiveRange = array_fill(0, rand(3, 5), 1); $integerKeys = [ 1 => 'foo', @@ -160,6 +167,12 @@ /** @var array $array */ $array = doFoo(); +/** @var array $array2 */ +$array2 = doFoo(); + +/** @var string[] $stringArray */ +$stringArray = doFoo(); + $slicedOffset = array_slice(['4' => 'foo', 1 => 'bar', 'baz' => 'qux', 0 => 'quux', 'quuz' => 'corge'], 0, null, false); $slicedOffsetWithKeys = array_slice(['4' => 'foo', 1 => 'bar', 'baz' => 'qux', 0 => 'quux', 'quuz' => 'corge'], 0, null, true); @@ -171,4 +184,7 @@ $mergedInts = array_merge($mergedInts, $generalIntegers); } +$fooArray = ['foo']; +$poppedFoo = array_pop($fooArray); + die; 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-shapes-keys-strings.php b/tests/PHPStan/Analyser/data/array-shapes-keys-strings.php deleted file mode 100644 index 996dd3e39e..0000000000 --- a/tests/PHPStan/Analyser/data/array-shapes-keys-strings.php +++ /dev/null @@ -1,24 +0,0 @@ - $dollar - */ - public function doFoo(array $slash, array $dollar): void - { - assertType("array('namespace/key' => string)", $slash); - assertType('array string)>', $dollar); - } - -} diff --git a/tests/PHPStan/Analyser/data/array-spread.php b/tests/PHPStan/Analyser/data/array-spread.php index 65de15eaef..f752a03270 100644 --- a/tests/PHPStan/Analyser/data/array-spread.php +++ b/tests/PHPStan/Analyser/data/array-spread.php @@ -1,4 +1,4 @@ -= 7.4 + $integersArray + * @param array $integersIterable */ public function doFoo( array $integersArray, 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 @@ += 7.4 += 7.4 + 1; + $y = fn(): array => ['a' => 1, 'b' => 2]; die; } diff --git a/tests/PHPStan/Analyser/data/assert-stub.php b/tests/PHPStan/Analyser/data/assert-stub.php new file mode 100644 index 0000000000..f4e3d6b353 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-stub.php @@ -0,0 +1,42 @@ +doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; + + +function (Bar $b, $x): void { + if ($b->doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert.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 @@ + 1, \'baz\' => 2)>', $array); - } - - public function doBar(int $i, int $j) - { - $array = []; - - $array[$i][$j]['bar'] = 1; - $array[$i][$j]['baz'] = 2; - - echo $array[$i][$j]['bar']; - echo $array[$i][$j]['baz']; - - assertType('array 1, \'baz\' => 2)>>', $array); - } - -} diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index 34ad22c812..cf6f245f5b 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -134,6 +134,15 @@ public function doFoo(array $generalArray) $arrayToBeUnset = $array; unset($arrayToBeUnset[$string]); + $arrayToBeUnset2 = $arrayToBeUnset; + unset($arrayToBeUnset2[$string]); + + $arrayToBeUnset3 = $array; + unset($arrayToBeUnset3[$integer]); + + $arrayToBeUnset4 = $arrayToBeUnset3; + unset($arrayToBeUnset4[$integer]); + /** @var array $shiftedNonEmptyArray */ $shiftedNonEmptyArray = doFoo(); @@ -167,6 +176,17 @@ public function doFoo(array $generalArray) $simpleXMLWritingXML = $simpleXML->asXML('path.xml'); + /** @var string $stringForXpath */ + $stringForXpath = doFoo(); + + $simpleXMLRightXpath = $simpleXML->xpath('/a/b/c'); + $simpleXMLWrongXpath = $simpleXML->xpath('[foo]'); + $simpleXMLUnknownXpath = $simpleXML->xpath($stringForXpath); + + $namespacedXML = new \SimpleXMLElement(''); + $namespacedXML->registerXPathNamespace('ns', 'namespace'); + $namespacedXpath = $namespacedXML->xpath('/ns:node'); + if (rand(0, 1)) { $maybeDefinedVariable = 'foo'; } @@ -176,6 +196,33 @@ public function doFoo(array $generalArray) $severalSumWithStaticConst2 = 1 + static::INT_CONST + 1; $severalSumWithStaticConst3 = 1 + 1 + static::INT_CONST; + if (!is_array($mixed)) { + $mixedNoArray = $mixed; + } + if (!is_int($mixed)) { + $mixedNoInt = $mixed; + } + if (!is_float($mixed)) { + $mixedNoFloat = $mixed; + } + if (!is_array($mixed)) { + if (!is_int($mixed)) { + $mixedNoArrayOrInt = $mixed; + } + } + + /** @var int|array $intOrArray */ + $intOrArray = doFoo(); + + /** @var array|float $floatOrArray */ + $floatOrArray = doFoo(); + + /** @var int|float $intOrFloat */ + $intOrFloat = doFoo(); + + /** @var array|float|int|string|bool $plusable */ + $plusable = doFoo(); + die; } diff --git a/tests/PHPStan/Analyser/data/bug-10049-recursive.php b/tests/PHPStan/Analyser/data/bug-10049-recursive.php new file mode 100644 index 0000000000..b0887157ba --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10049-recursive.php @@ -0,0 +1,66 @@ + + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} + +/** + * @template-extends SimpleEntity + */ +class TestEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(TestEntity::class, 'testentity'); + parent::__construct($table); + } +} + + +/** + * @template-extends SimpleEntity + */ +class AnotherEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(AnotherEntity::class, 'anotherentity'); + parent::__construct($table); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-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 @@ +|int<3, max>', $nullable); + break; + } + + return $nullable; +} 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-1843.php b/tests/PHPStan/Analyser/data/bug-1843.php new file mode 100644 index 0000000000..7e2b1f201b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1843.php @@ -0,0 +1,24 @@ + [ + 'A' => '2', + 'B' => '3', + 'C' => '4', + 'D' => '5', + 'E' => '6', + 'F' => '7', + ], + ]; + + public function sayHello(): void + { + echo self::P[self::W]['A']; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1871.php b/tests/PHPStan/Analyser/data/bug-1871.php new file mode 100644 index 0000000000..9f8ec3cf10 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1871.php @@ -0,0 +1,17 @@ + string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $parsedUrl); - - if (array_key_exists('host', $parsedUrl)) { - assertType('array(?\'scheme\' => string, \'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)', $parsedUrl); - throw new \RuntimeException('Absolute URLs are prohibited for the redirectTo parameter.'); - } - - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $parsedUrl); - - $redirectUrl = $parsedUrl['path']; - - if (array_key_exists('query', $parsedUrl)) { - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, \'query\' => string, ?\'fragment\' => string)', $parsedUrl); - $redirectUrl .= '?' . $parsedUrl['query']; - } - - if (array_key_exists('fragment', $parsedUrl)) { - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, \'fragment\' => string)', $parsedUrl); - $redirectUrl .= '#' . $parsedUrl['query']; - } - - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'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-2232.php b/tests/PHPStan/Analyser/data/bug-2232.php deleted file mode 100644 index d2b06166df..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2232.php +++ /dev/null @@ -1,39 +0,0 @@ - "a", - 'a2' => "b", - 'a3' => "c", - 'a4' => [ - 'name' => "dsfs", - 'version' => "fdsfs", - ], - ]; - - if (rand(0, 1)) { - $data['b1'] = "hello"; - } - - if (rand(0, 1)) { - $data['b2'] = "hello"; - } - - if (rand(0, 1)) { - $data['b3'] = "hello"; - } - - if (rand(0, 1)) { - $data['b4'] = "goodbye"; - } - - if (rand(0, 1)) { - $data['b5'] = "env"; - } - - assertType('array(\'a1\' => \'a\', \'a2\' => \'b\', \'a3\' => \'c\', \'a4\' => array(\'name\' => \'dsfs\', \'version\' => \'fdsfs\'), ?\'b1\' => \'hello\', ?\'b2\' => \'hello\', ?\'b3\' => \'hello\', ?\'b4\' => \'goodbye\', ?\'b5\' => \'env\')', $data); -}; diff --git a/tests/PHPStan/Analyser/data/bug-2600.php b/tests/PHPStan/Analyser/data/bug-2600.php deleted file mode 100644 index d708f65536..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2600.php +++ /dev/null @@ -1,88 +0,0 @@ -', $x); - } - - /** - * @param mixed ...$x - */ - public function doLorem(...$x) { - assertType('array', $x); - } - - public function doIpsum($x = null) { - $args = func_get_args(); - assertType('mixed', $x); - assertType('array', $args); - } -} - -class Bar -{ - /** - * @param string ...$x - */ - public function doFoo($x = null) { - $args = func_get_args(); - assertType('string|null', $x); - assertType('array', $args); - } - - /** - * @param string ...$x - */ - public function doBar($x = null) { - assertType('string|null', $x); - } - - /** - * @param string $x - */ - public function doBaz(...$x) { - assertType('array', $x); - } - - /** - * @param string ...$x - */ - public function doLorem(...$x) { - assertType('array', $x); - } -} - -function foo($x, string ...$y): void -{ - assertType('mixed', $x); - assertType('array', $y); -} - -function ($x, string ...$y): void { - assertType('mixed', $x); - assertType('array', $y); -}; diff --git a/tests/PHPStan/Analyser/data/bug-2648.php b/tests/PHPStan/Analyser/data/bug-2648.php deleted file mode 100644 index 25828c0158..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2648.php +++ /dev/null @@ -1,51 +0,0 @@ - 1) { - assertType('int<2, max>', count($list)); - unset($list['fooo']); - assertType('array', $list); - assertType('int', count($list)); - } - } - - /** - * @param bool[] $list - */ - public function doBar(array $list): void - { - if (count($list) > 1) { - assertType('int<2, max>', count($list)); - foreach ($list as $key => $item) { - assertType('int<2, max>|int', count($list)); - if ($item === false) { - unset($list[$key]); - assertType('int', count($list)); - } - - assertType('int', count($list)); - - if (count($list) === 1) { - assertType('int', count($list)); - break; - } - } - - assertType('int', count($list)); - } - - assertType('int', count($list)); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-2677.php b/tests/PHPStan/Analyser/data/bug-2677.php deleted file mode 100644 index 1ea7aca573..0000000000 --- a/tests/PHPStan/Analyser/data/bug-2677.php +++ /dev/null @@ -1,49 +0,0 @@ - 0); - assertType('int<1, max>', count($input)); - array_shift($input); - assertType('int', count($input)); - - \assert(count($input) > 0); - assertType('int<1, max>', count($input)); - array_pop($input); - assertType('int', count($input)); - - \assert(count($input) > 0); - assertType('int<1, max>', count($input)); - array_unshift($input, 'test'); - assertType('int', count($input)); - - \assert(count($input) > 0); - assertType('int<1, max>', count($input)); - array_push($input, 'nope'); - assertType('int', count($input)); -}; diff --git a/tests/PHPStan/Analyser/data/bug-2954.php b/tests/PHPStan/Analyser/data/bug-2954.php index 2a8a1e1ba1..73b21239e6 100644 --- a/tests/PHPStan/Analyser/data/bug-2954.php +++ b/tests/PHPStan/Analyser/data/bug-2954.php @@ -2,11 +2,11 @@ namespace Analyser\Bug2954; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; function (int $x) { if ($x === 0) return; - assertType('int<1, max>|int', $x); + assertType('int|int<1, max>', $x); $x++; assertType('int', $x); @@ -14,7 +14,7 @@ function (int $x) { function (int $x) { if ($x === 0) return; - assertType('int<1, max>|int', $x); + assertType('int|int<1, max>', $x); ++$x; assertType('int', $x); @@ -22,7 +22,7 @@ function (int $x) { function (int $x) { if ($x === 0) return; - assertType('int<1, max>|int', $x); + assertType('int|int<1, max>', $x); $x--; assertType('int', $x); @@ -30,7 +30,7 @@ function (int $x) { function (int $x) { if ($x === 0) return; - assertType('int<1, max>|int', $x); + assertType('int|int<1, max>', $x); --$x; assertType('int', $x); diff --git a/tests/PHPStan/Analyser/data/bug-3009.php b/tests/PHPStan/Analyser/data/bug-3009.php deleted file mode 100644 index fcaa76419d..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3009.php +++ /dev/null @@ -1,30 +0,0 @@ - string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $redirectUrlParts); - return null; - } - - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)', $redirectUrlParts); - - if (true === array_key_exists('query', $redirectUrlParts)) { - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, \'query\' => string, ?\'fragment\' => string)', $redirectUrlParts); - $redirectServer['QUERY_STRING'] = $redirectUrlParts['query']; - } - - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)', $redirectUrlParts); - - return 'foo'; - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3142.php b/tests/PHPStan/Analyser/data/bug-3142.php deleted file mode 100644 index 4f3782da92..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3142.php +++ /dev/null @@ -1,73 +0,0 @@ -sayHi()); -assertType('int', $hw->sayHello()); - -interface DecoratorInterface -{ -} - -class FooDecorator implements DecoratorInterface -{ - public function getCode(): string - { - return 'FOO'; - } -} - -trait DecoratorTrait -{ - public function getDecorator(): DecoratorInterface - { - return new FooDecorator(); - } -} - -/** - * @method FooDecorator getDecorator() - */ -class Dummy -{ - use DecoratorTrait; - - public function getLabel(): string - { - return $this->getDecorator()->getCode(); - } -} - -$dummy = new Dummy(); -assertType(FooDecorator::class, $dummy->getDecorator()); diff --git a/tests/PHPStan/Analyser/data/bug-3266.php b/tests/PHPStan/Analyser/data/bug-3266.php deleted file mode 100644 index 6311d33d2c..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3266.php +++ /dev/null @@ -1,32 +0,0 @@ - $iterator - * @phpstan-return array - */ - public function iteratorToArray($iterator) - { - assertType('array', $iterator); - $array = []; - foreach ($iterator as $key => $value) { - assertType('TKey (method Bug3266\Foo::iteratorToArray(), argument)', $key); - assertType('TValue (method Bug3266\Foo::iteratorToArray(), argument)', $value); - $array[$key] = $value; - assertType('array', $array); - } - - assertType('array', $array); - - return $array; - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3269.php b/tests/PHPStan/Analyser/data/bug-3269.php deleted file mode 100644 index 09e21f2e0d..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3269.php +++ /dev/null @@ -1,46 +0,0 @@ -> $intervalGroups - */ - public static function bar(array $intervalGroups): void - { - $borders = []; - foreach ($intervalGroups as $group) { - foreach ($group as $interval) { - $borders[] = ['version' => $interval['start']->getVersion(), 'operator' => $interval['start']->getOperator(), 'side' =>'start']; - $borders[] = ['version' => $interval['end']->getVersion(), 'operator' => $interval['end']->getOperator(), 'side' =>'end']; - } - } - - assertType('array string, \'operator\' => string, \'side\' => \'end\'|\'start\')>', $borders); - - foreach ($borders as $border) { - assertType('array(\'version\' => string, \'operator\' => string, \'side\' => \'end\'|\'start\')', $border); - assertType('\'end\'|\'start\'', $border['side']); - } - } - -} - -class Blah -{ - - public function getVersion(): string - { - return ''; - } - - public function getOperator(): string - { - return ''; - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3276.php b/tests/PHPStan/Analyser/data/bug-3276.php deleted file mode 100644 index 3ba4408b83..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3276.php +++ /dev/null @@ -1,28 +0,0 @@ -= 7.4 - -namespace Bug3276; - -use function PHPStan\Analyser\assertType; - -class Foo -{ - - /** - * @param array{name?:string} $settings - */ - public function doFoo(array $settings): void - { - $settings['name'] ??= 'unknown'; - assertType('array(\'name\' => string)', $settings); - } - - /** - * @param array{name?:string} $settings - */ - public function doBar(array $settings): void - { - $settings['name'] = 'unknown'; - assertType('array(\'name\' => \'unknown\')', $settings); - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-3300.php b/tests/PHPStan/Analyser/data/bug-3300.php new file mode 100644 index 0000000000..c5614b868a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3300.php @@ -0,0 +1,44 @@ + '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-3309.php b/tests/PHPStan/Analyser/data/bug-3309.php new file mode 100644 index 0000000000..cce0012e4d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3309.php @@ -0,0 +1,18 @@ +experience > self::BAR) { + $this->experience = self::BAR; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3336.php b/tests/PHPStan/Analyser/data/bug-3336.php deleted file mode 100644 index 65869e3b8f..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3336.php +++ /dev/null @@ -1,10 +0,0 @@ -', mb_convert_encoding($arr)); - \PHPStan\Analyser\assertType('string', mb_convert_encoding($str)); - \PHPStan\Analyser\assertType('array|string', mb_convert_encoding($mixed)); - \PHPStan\Analyser\assertType('array|string', mb_convert_encoding()); -}; diff --git a/tests/PHPStan/Analyser/data/bug-3468.php b/tests/PHPStan/Analyser/data/bug-3468.php new file mode 100644 index 0000000000..b702618756 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3468.php @@ -0,0 +1,19 @@ +f = 0.1; +}; + +class NewDocument extends \DOMDocument +{ +} + +function (NewDocument $nd): void { + $element = $nd->documentElement; +}; diff --git a/tests/PHPStan/Analyser/data/bug-3686.php b/tests/PHPStan/Analyser/data/bug-3686.php new file mode 100644 index 0000000000..586a62a771 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3686.php @@ -0,0 +1,39 @@ + + */ +class RecursiveClass extends EntityRepository +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-3909.php b/tests/PHPStan/Analyser/data/bug-3909.php new file mode 100644 index 0000000000..3dd72bcf2f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3909.php @@ -0,0 +1,7 @@ + + */ +interface QueryHandlerInterface +{ + /** + * @param TQuery $query + * + * @return TResult + */ + public function handle(QueryInterface $query); +} + +/** + * @template TResult + */ +interface QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class FooQuery implements QueryInterface +{ +} + +final class FooQueryResult +{ +} + +/** + * @template-implements QueryHandlerInterface + */ +final class FooQueryHandler implements QueryHandlerInterface +{ + public function handle(QueryInterface $query): FooQueryResult + { + return new FooQueryResult(); + } +} + +interface BasePackage {} + +interface InnerPackage extends BasePackage {} + +/** + * @template TInnerPackage of InnerPackage + */ +interface GenericPackage extends BasePackage { + /** @return TInnerPackage */ + public function unwrap() : InnerPackage; +} + +interface SomeInnerPackage extends InnerPackage {} + +/** + * @extends GenericPackage + */ +interface SomePackage extends GenericPackage {} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param TGenericPackage $package + * @return TInnerPackage + */ +function unwrapGeneric(GenericPackage $package) { + return $package->unwrap(); +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TInnerPackage + */ +function loadWithDirectUnwrap(string $class) { + $package = new $class(); + return $package->unwrap(); +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TInnerPackage + */ +function loadWithIndirectUnwrap(string $class) { + $package = new $class(); + return unwrapGeneric($package); +} + +function (): void { + loadWithDirectUnwrap(SomePackage::class); +}; diff --git a/tests/PHPStan/Analyser/data/bug-4097.php b/tests/PHPStan/Analyser/data/bug-4097.php new file mode 100644 index 0000000000..c18ad8ec14 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4097.php @@ -0,0 +1,45 @@ +entities = $entities; + } + + /** + * @return \Traversable + */ + public function findAllSnapshots(): \Traversable + { + yield from \array_map( + \Closure::fromCallable([$this, 'buildSnapshot']), + $this->entities + ); + } + + /** + * @param Fu|Bar $entity + * @phpstan-param T $entity + */ + public function buildSnapshot($entity): Snapshot + { + return new Snapshot(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4288.php b/tests/PHPStan/Analyser/data/bug-4288.php new file mode 100644 index 0000000000..10c2318731 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4288.php @@ -0,0 +1,28 @@ +paginate(); + } +} + diff --git a/tests/PHPStan/Analyser/data/bug-4300.php b/tests/PHPStan/Analyser/data/bug-4300.php new file mode 100644 index 0000000000..46895b0456 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4300.php @@ -0,0 +1,18 @@ + count($column2) ? 2 : 1; + + return $column; + } + +} 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-4513.php b/tests/PHPStan/Analyser/data/bug-4513.php new file mode 100644 index 0000000000..23604269f4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4513.php @@ -0,0 +1,27 @@ + + */ +interface Collection extends \IteratorAggregate {} + +/** + * @phpstan-template TKey + * @phpstan-template T + * @template-implements Collection + */ +class ArrayCollection implements Collection +{ + /** + * {@inheritDoc} + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} + +class Administration {} + +class Company +{ + /** + * @var Collection + */ + protected Collection $administrations; + + public function __construct() + { + $this->administrations = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getAdministrations() : Collection + { + return $this->administrations; + } +} 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-4734.php b/tests/PHPStan/Analyser/data/bug-4734.php new file mode 100644 index 0000000000..8f7d6af714 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4734.php @@ -0,0 +1,60 @@ +httpMethodParameterOverride2; + } +} + +class Bar +{ + public function test(): void + { + $disableHttpMethodParameterOverride = \Closure::bind(static function (): void { + static::$httpMethodParameterOverride = false; + }, new Foo(), Foo::class); + $disableHttpMethodParameterOverride(); + + $disableHttpMethodParameterOverride2 = \Closure::bind(function (): void { + $this->httpMethodParameterOverride2 = false; + }, new Foo(), Foo::class); + $disableHttpMethodParameterOverride2(); + + $disableHttpMethodParameterOverride3 = \Closure::bind(function (): void { + static::$httpMethodParameterOverride3 = false; + }, new Foo(), Foo::class); + $disableHttpMethodParameterOverride3(); + + $disableHttpMethodParameterOverride4 = \Closure::bind(function (): void { + $this->httpMethodParameterOverride4 = false; + }, new Foo(), Foo::class); + $disableHttpMethodParameterOverride4(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4902.php b/tests/PHPStan/Analyser/data/bug-4902.php new file mode 100644 index 0000000000..fc84a47d33 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4902.php @@ -0,0 +1,53 @@ +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('list', array_map(function (Wrapper $item) { + return $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-5231.php b/tests/PHPStan/Analyser/data/bug-5231.php new file mode 100644 index 0000000000..f42aebe125 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5231.php @@ -0,0 +1,78 @@ +collection as $item) { + if ((string)$item === $name) { + return $item; + } + } + + return null; + } + + // must be exists! + public function existsByKey(string $name): bool + { + return $this->findByName($name) !== null; + } + + public function getSorted(callable $comparator): self + { + $sortedCollection = $this->collection; + usort($sortedCollection, $comparator); + + $filtered = array_values($sortedCollection); + + return new static(...$filtered); + } + + public function rewind(): void + { + reset($this->collection); + } + + public function current() + { + return current($this->collection); + } + + /** + * @return bool|float|int|string|null + */ + public function key() + { + return key($this->collection); + } + + /** + * @return mixed|void + */ + public function next() + { + return next($this->collection); + } + + public function valid(): bool + { + return isset($this->collection[key($this->collection)]); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5231_2.php b/tests/PHPStan/Analyser/data/bug-5231_2.php new file mode 100644 index 0000000000..07e65e5b26 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5231_2.php @@ -0,0 +1,78 @@ +collection as $item) { + if ((string)$item === $name) { + return $item; + } + } + + return null; + } + + // must be exists! + public function existsByKey(string $name): bool + { + return $this->findByName($name) !== null; + } + + public function getSorted(callable $comparator): self + { + $sortedCollection = $this->collection; + usort($sortedCollection, $comparator); + + $filtered = array_values($sortedCollection); + + return new static(...$filtered); + } + + public function rewind(): void + { + reset($this->collection); + } + + public function current() + { + return current($this->collection); + } + + /** + * @return bool|float|int|string|null + */ + public function key() + { + return key($this->collection); + } + + /** + * @return mixed|void + */ + public function next() + { + return next($this->collection); + } + + public function valid(): bool + { + return isset($this->collection[key($this->collection)]); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-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-5527.php b/tests/PHPStan/Analyser/data/bug-5527.php new file mode 100644 index 0000000000..076e6626fc --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5527.php @@ -0,0 +1,20 @@ += 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-5639.php b/tests/PHPStan/Analyser/data/bug-5639.php new file mode 100644 index 0000000000..c0eeed3370 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5639.php @@ -0,0 +1,22 @@ +message, $this->file, $this->line); + return $result; + } + } +} +else +{ + class Foo extends \Error { + function __toString(): string { + $result = \sprintf("%s\n\nin %s on line %s", $this->message, $this->file, $this->line); + return $result; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5657.php b/tests/PHPStan/Analyser/data/bug-5657.php new file mode 100644 index 0000000000..7cb6fa91f0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5657.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug5951; + +#[\Attribute] +class Route +{ + + /** @param string[] $methods */ + public function __construct(public string $path, public string $name, public array $methods) + { + + } + +} + +class Response +{ + +} + +final class SomeController +{ + public const ROUTE_INDEX = 'some_index'; + + #[Route('/some', name: self::ROUTE_INDEX, methods: ['GET'])] + public function index(): Response + { + return new Response(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6114.php b/tests/PHPStan/Analyser/data/bug-6114.php new file mode 100644 index 0000000000..73b7c4f507 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6114.php @@ -0,0 +1,45 @@ += 8.0 + +namespace Bug6114; + +/** + * @template T + */ +interface Foo { + /** + * @return T + */ + public function bar(): mixed; +} + +class HelloWorld +{ + /** + * @template T + * @param T $value + * @return Foo + */ + public function sayHello(mixed $value): Foo + { + return new + /** + * @template U + * @implements Foo + */ class($value) implements Foo { + /** @var U */ + private mixed $value; + + /** + * @param U $value + */ + public function __construct(mixed $value) { + $this->value = $value; + } + + public function bar(): mixed + { + return $this->value; + } + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-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-6212.php b/tests/PHPStan/Analyser/data/bug-6212.php new file mode 100644 index 0000000000..a9aa125bde --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6212.php @@ -0,0 +1,13 @@ +pgsqlGetNotify(\PDO::FETCH_ASSOC); diff --git a/tests/PHPStan/Analyser/data/bug-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-6300.php b/tests/PHPStan/Analyser/data/bug-6300.php new file mode 100644 index 0000000000..c9244c6cec --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6300.php @@ -0,0 +1,26 @@ +get(); + echo $b->fooProp; +}; + diff --git a/tests/PHPStan/Analyser/data/bug-6375.php b/tests/PHPStan/Analyser/data/bug-6375.php new file mode 100644 index 0000000000..80f9d9b1bf --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6375.php @@ -0,0 +1,19 @@ + $i + * @return void + */ + public function sayHello($i): void + { + $a = []; + $a[$i] = 5; + assertType('non-empty-array, 5>', $a); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6442.php b/tests/PHPStan/Analyser/data/bug-6442.php new file mode 100644 index 0000000000..2bcd2309ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6442.php @@ -0,0 +1,24 @@ += 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-6501.php b/tests/PHPStan/Analyser/data/bug-6501.php new file mode 100644 index 0000000000..4254ae62c8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6501.php @@ -0,0 +1,34 @@ + + */ +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-6681.php b/tests/PHPStan/Analyser/data/bug-6681.php new file mode 100644 index 0000000000..836f247d91 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6681.php @@ -0,0 +1,18 @@ + []]; +} + + + +$apiCacheMap = new class extends ApiCacheMap { + protected const CACHE_MAP = [ + 1 => ApiCacheMap::CACHE_MAP[self::DEFAULT_CACHE_TTL], + ]; +}; diff --git a/tests/PHPStan/Analyser/data/bug-6740-a.php b/tests/PHPStan/Analyser/data/bug-6740-a.php new file mode 100644 index 0000000000..b244f217f6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6740-a.php @@ -0,0 +1,53 @@ +StdClassSetup(get_class()); + } +} + +class A +{ + /** @var string[] */ + + private $classList = []; + + /** + * @returns $this + */ + + public function __construct() + { + } + + /** + * Apply all the standard configuration needs for a sub-class + * + * @param string $baseClass + */ + + public function StdClassSetup($baseClass): void + { + $this->classList[] = $baseClass; + } + + /** + * @return string[] + */ + + public function GetClassList() + { + return $this->classList; + } +} + +class Box extends A +{ + use BlockTemplate; +} diff --git a/tests/PHPStan/Analyser/data/bug-6740-b.php b/tests/PHPStan/Analyser/data/bug-6740-b.php new file mode 100644 index 0000000000..f4a9b44b6b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6740-b.php @@ -0,0 +1,8 @@ + + */ + 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/cast-unset.php b/tests/PHPStan/Analyser/data/cast-unset.php deleted file mode 100644 index 701bd9834c..0000000000 --- a/tests/PHPStan/Analyser/data/cast-unset.php +++ /dev/null @@ -1,14 +0,0 @@ -bindTo($newThis); -assertType('Closure(object): bool', $boundClosure); - -$staticallyBoundClosure = \Closure::bind($closure, $newThis); -assertType('Closure(object): bool', $staticallyBoundClosure); - -$returnType = $closure->call($newThis, new class {}); -assertType('bool', $returnType); 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 @@ + $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/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 @@ +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-compound-types.php b/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php index 0b9f1d5a7d..5d163a1eda 100644 --- a/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php @@ -2,13 +2,12 @@ namespace DynamicMethodReturnCompoundTypes; -class Collection -{ +use function PHPStan\Testing\assertType; - public function getSelf() - { +interface Collection extends \Traversable +{ - } + public function getSelf(); } @@ -26,7 +25,8 @@ public function getSelf() */ public function doFoo($collection, $collectionOrFoo) { - die; + assertType('DynamicMethodReturnCompoundTypes\Collection', $collection->getSelf()); + assertType('DynamicMethodReturnCompoundTypes\Collection|DynamicMethodReturnCompoundTypes\Foo', $collectionOrFoo->getSelf()); } } diff --git a/tests/PHPStan/Analyser/data/dynamic-method-return-types-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-return-types.php b/tests/PHPStan/Analyser/data/dynamic-method-return-types.php index e32d36e329..0fb7c70d6d 100644 --- a/tests/PHPStan/Analyser/data/dynamic-method-return-types.php +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-types.php @@ -2,6 +2,8 @@ namespace DynamicMethodReturnTypesNamespace; +use function PHPStan\Testing\assertType; + class EntityManager { @@ -25,6 +27,7 @@ class InheritedEntityManager extends EntityManager class ComponentContainer implements \ArrayAccess { + #[\ReturnTypeWillChange] public function offsetExists($offset) { @@ -35,11 +38,13 @@ public function offsetGet($offset): Entity } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { @@ -59,7 +64,30 @@ public function doFoo() $em = new EntityManager(); $iem = new InheritedEntityManager(); $container = new ComponentContainer(); - die; + + assertType('*ERROR*', $em->getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\Entity', $em->getByPrimary()); + assertType('DynamicMethodReturnTypesNamespace\Entity', $em->getByPrimary($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', $em->getByPrimary(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('*ERROR*', $iem->getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\Entity', $iem->getByPrimary()); + assertType('DynamicMethodReturnTypesNamespace\Entity', $iem->getByPrimary($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', $iem->getByPrimary(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('*ERROR*', EntityManager::getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity()); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('*ERROR*', InheritedEntityManager::getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity()); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('DynamicMethodReturnTypesNamespace\Foo', $container[\DynamicMethodReturnTypesNamespace\Foo::class]); + assertType('object', new \DynamicMethodReturnTypesNamespace\Foo()); + assertType('object', new \DynamicMethodReturnTypesNamespace\FooWithoutConstructor()); } } diff --git a/tests/PHPStan/Analyser/data/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/dynamic-method-throw-type-extension.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension.php new file mode 100644 index 0000000000..9d4aaf48a3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension.php @@ -0,0 +1,123 @@ +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; + } + +} + +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(true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo2() + { + try { + $result = $this->throwOrNot(false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + + public function doFoo3() + { + try { + $result = self::staticThrowOrNot(true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo4() + { + try { + $result = self::staticThrowOrNot(false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/early-termination-defined.php b/tests/PHPStan/Analyser/data/early-termination-defined.php index 09e04c510d..d1367f1c2b 100644 --- a/tests/PHPStan/Analyser/data/early-termination-defined.php +++ b/tests/PHPStan/Analyser/data/early-termination-defined.php @@ -14,6 +14,21 @@ public function doFoo() throw new \Exception(); } + /** + * @return no-return + */ + public static function doBarPhpDoc() + { + throw new \Exception(); + } + + /** + * @return never-return + */ + public function doFooPhpDoc() + { + throw new \Exception(); + } } class Bar extends Foo @@ -25,3 +40,19 @@ function baz() { throw new \Exception(); } + +/** + * @return never + */ +function bazPhpDoc() +{ + throw new \Exception(); +} + +/** + * @return never-returns + */ +function bazPhpDoc2() +{ + throw new \Exception(); +} 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-integration.php b/tests/PHPStan/Analyser/data/enums-integration.php new file mode 100644 index 0000000000..a7b50c774b --- /dev/null +++ b/tests/PHPStan/Analyser/data/enums-integration.php @@ -0,0 +1,85 @@ += 8.1 + +namespace EnumIntegrationTest; + +enum Foo +{ + + case ONE; + case TWO; + +} + + +class FooClass +{ + + public function doFoo(Foo $foo): void + { + $this->doBar($foo); + $this->doBar(Foo::ONE); + $this->doBar(Foo::TWO); + echo Foo::TWO->value; + echo count(Foo::cases()); + } + + public function doBar(Foo $foo): void + { + + } + +} + +enum Bar : string +{ + + case ONE = 'one'; + case TWO = 'two'; + +} + +class BarClass +{ + + public function doFoo(Bar $bar, string $s): void + { + $this->doBar($bar); + $this->doBar(Bar::ONE); + $this->doBar(Bar::TWO); + $this->doBar(Bar::NONEXISTENT); + echo Bar::TWO->value; + echo count(Bar::cases()); + $this->doBar(Bar::from($s)); + $this->doBar(Bar::tryFrom($s)); + } + + public function doBar(?Bar $bar): void + { + + } + +} + +enum Baz : int +{ + + case ONE = 1; + case TWO = 2; + const THREE = 3; + const FOUR = 4; + +} + +class Lorem +{ + + public function doBaz(Foo $foo): void + { + if ($foo === Foo::ONE) { + if ($foo === Foo::TWO) { + + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/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/finally-with-early-termination.php b/tests/PHPStan/Analyser/data/finally-with-early-termination.php index 5a56722f41..3a00cc8113 100644 --- a/tests/PHPStan/Analyser/data/finally-with-early-termination.php +++ b/tests/PHPStan/Analyser/data/finally-with-early-termination.php @@ -5,6 +5,7 @@ try { $integerOrString = 1; $fooOrBarException = null; + maybeThrows(); return 1; } catch (FooException $e) { $integerOrString = 1; diff --git a/tests/PHPStan/Analyser/data/finally.php b/tests/PHPStan/Analyser/data/finally.php index 10f4744751..35db3bd0fb 100644 --- a/tests/PHPStan/Analyser/data/finally.php +++ b/tests/PHPStan/Analyser/data/finally.php @@ -16,6 +16,7 @@ function () { try { $integerOrString = 1; $fooOrBarException = null; + maybeThrows(); } catch (FooException $e) { $integerOrString = 1; $fooOrBarException = $e; diff --git a/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php b/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php index 1d5f8df694..926e501576 100644 --- a/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php +++ b/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php @@ -1,6 +1,6 @@ $value) { + die; +} diff --git a/tests/PHPStan/Analyser/data/functionPhpDocs-phanPrefix.php b/tests/PHPStan/Analyser/data/functionPhpDocs-phanPrefix.php new file mode 100644 index 0000000000..bb568e65a9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/functionPhpDocs-phanPrefix.php @@ -0,0 +1,79 @@ +doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } +} diff --git a/tests/PHPStan/Analyser/data/functionPhpDocs-phpstanPrefix.php b/tests/PHPStan/Analyser/data/functionPhpDocs-phpstanPrefix.php index 609e14c207..56dc11ceee 100644 --- a/tests/PHPStan/Analyser/data/functionPhpDocs-phpstanPrefix.php +++ b/tests/PHPStan/Analyser/data/functionPhpDocs-phpstanPrefix.php @@ -52,7 +52,6 @@ function doFooPhpstanPrefix( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $nullType, $barObject, Bar $conflictedObject, @@ -68,7 +67,8 @@ function doFooPhpstanPrefix( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $fooFunctionResult = doFoo(); diff --git a/tests/PHPStan/Analyser/data/functionPhpDocs-psalmPrefix.php b/tests/PHPStan/Analyser/data/functionPhpDocs-psalmPrefix.php index 00e065f13e..60a0f87076 100644 --- a/tests/PHPStan/Analyser/data/functionPhpDocs-psalmPrefix.php +++ b/tests/PHPStan/Analyser/data/functionPhpDocs-psalmPrefix.php @@ -52,7 +52,6 @@ function doFooPsalmPrefix( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $nullType, $barObject, Bar $conflictedObject, @@ -68,7 +67,8 @@ function doFooPsalmPrefix( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $fooFunctionResult = doFoo(); diff --git a/tests/PHPStan/Analyser/data/functionPhpDocs.php b/tests/PHPStan/Analyser/data/functionPhpDocs.php index a195e2be59..5b3c5feaf6 100644 --- a/tests/PHPStan/Analyser/data/functionPhpDocs.php +++ b/tests/PHPStan/Analyser/data/functionPhpDocs.php @@ -52,7 +52,6 @@ function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $nullType, $barObject, Bar $conflictedObject, @@ -68,7 +67,8 @@ function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $fooFunctionResult = doFoo(); diff --git a/tests/PHPStan/Analyser/data/functions.php b/tests/PHPStan/Analyser/data/functions.php index 7c36d6e854..ff580ad6a4 100644 --- a/tests/PHPStan/Analyser/data/functions.php +++ b/tests/PHPStan/Analyser/data/functions.php @@ -6,12 +6,6 @@ $microtimeDefault = microtime(null); $microtimeBenevolent = microtime($undefined); -$strtotimeNow = strtotime('now'); -$strtotimeInvalid = strtotime('4 qm'); -$strtotimeUnknown = strtotime(doFoo() ? 'now': '4 qm'); -$strtotimeUnknown2 = strtotime($undefined); -$strtotimeCrash = strtotime(); - $versionCompare1 = version_compare('7.0.0', '7.0.1'); $versionCompare2 = version_compare('7.0.0', doFoo() ? '7.0.1' : '6.0.0'); $versionCompare3 = version_compare(doFoo() ? '7.0.0' : '6.0.5', doBar() ? '7.0.1' : '6.0.0'); @@ -21,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'); @@ -101,6 +88,8 @@ $stat = stat(__FILE__); $lstat = lstat(__FILE__); $fstat = fstat($resource); +$fileObject = new \SplFileObject(__FILE__); +$fileObjectStat = $fileObject->fstat(); $base64DecodeWithoutStrict = base64_decode(''); $base64DecodeWithStrictDisabled = base64_decode('', false); diff --git a/tests/PHPStan/Analyser/data/generic-class-string.php b/tests/PHPStan/Analyser/data/generic-class-string.php deleted file mode 100644 index 84d74d2d36..0000000000 --- a/tests/PHPStan/Analyser/data/generic-class-string.php +++ /dev/null @@ -1,122 +0,0 @@ -|DateTimeInterface', $a); - assertType('DateTimeInterface', new $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()); - } - - if (is_subclass_of($a, C::class)) { - assertType('int', $a::f()); - } -} - -/** - * @param object $a - */ -function testObject($a) { - assertType('mixed', new $a()); - - if (is_subclass_of($a, 'DateTimeInterface')) { - assertType('DateTimeInterface', $a); - } -} - -/** - * @param string $a - */ -function testString($a) { - assertType('mixed', new $a()); - - if (is_subclass_of($a, 'DateTimeInterface')) { - assertType('class-string', $a); - assertType('DateTimeInterface', new $a()); - } - - if (is_subclass_of($a, C::class)) { - assertType('int', $a::f()); - } -} - -/** - * @param string|object $a - */ -function testStringObject($a) { - assertType('mixed', new $a()); - - if (is_subclass_of($a, 'DateTimeInterface')) { - assertType('class-string|DateTimeInterface', $a); - assertType('DateTimeInterface', new $a()); - } - - if (is_subclass_of($a, C::class)) { - assertType('int', $a::f()); - } -} - -/** - * @param class-string<\DateTimeInterface> $a - */ -function testClassString($a) { - assertType('DateTimeInterface', new $a()); - - if (is_subclass_of($a, 'DateTime')) { - assertType('class-string', $a); - assertType('DateTime', new $a()); - } -} - -function testClassExists(string $str) -{ - assertType('string', $str); - if (class_exists($str)) { - assertType('class-string', $str); - } - - $existentClass = \stdClass::class; - if (class_exists($existentClass)) { - assertType('\'stdClass\'', $existentClass); - } - - $nonexistentClass = 'NonexistentClass'; - if (class_exists($nonexistentClass)) { - assertType('\'NonexistentClass\'', $nonexistentClass); - } -} - -function testInterfaceExists(string $str) -{ - assertType('string', $str); - if (interface_exists($str)) { - assertType('class-string', $str); - } -} - -function testTraitExists(string $str) -{ - assertType('string', $str); - if (trait_exists($str)) { - assertType('class-string', $str); - } -} diff --git a/tests/PHPStan/Analyser/data/if-defined.php b/tests/PHPStan/Analyser/data/if-defined.php index 81832db199..f0523beb8b 100644 --- a/tests/PHPStan/Analyser/data/if-defined.php +++ b/tests/PHPStan/Analyser/data/if-defined.php @@ -5,21 +5,25 @@ class Foo implements \ArrayAccess { + #[\ReturnTypeWillChange] public function offsetExists($offset) { } + #[\ReturnTypeWillChange] public function offsetGet($offset) { } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { diff --git a/tests/PHPStan/Analyser/data/if.php b/tests/PHPStan/Analyser/data/if.php index 96ddffb2c8..ee0a7a24e3 100644 --- a/tests/PHPStan/Analyser/data/if.php +++ b/tests/PHPStan/Analyser/data/if.php @@ -38,8 +38,8 @@ function () { $exceptionFromTry = null; try { $inTry = 1; - $inTryNotInCatch = 1; $fooObjectFromTryCatch = new InTryCatchFoo(); + $inTryNotInCatch = 1; $mixedVarFromTryCatch = 1; $nullableIntegerFromTryCatch = 1; $anotherNullableIntegerFromTryCatch = null; @@ -59,7 +59,7 @@ function () { $exceptionFromTryCatch = null; try { - + maybeThrows(); } catch (\SomeConcreteException $exceptionFromTryCatch) { return; } catch (\AnotherException $exceptionFromTryCatch) { @@ -351,6 +351,7 @@ function () { try { $inTryTwo = 1; + maybeThrows(); } catch (\Exception $e) { $exception = $e; if (something()) { 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/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<3, max>|int', $i); - - if ($i < 3 && $i > 5) { - assertType('*NEVER*', $i); - } else { - assertType('int<3, max>|int', $i); - } - - if ($i > 3 && $i < 5) { - assertType('4', $i); - } else { - assertType('3|int<5, max>|int', $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', $i); // should improved to be int<0, 4> - } - - $i = 0; - while ($i < 5) { - assertType('int', $i); // should improved to be int<0, 4> - $i++; - } - - $i = 0; - while ($i++ < 5) { - assertType('int', $i); // should improved to be int<1, 5> - } - - $i = 0; - while (++$i < 5) { - assertType('int', $i); // should improved to be int<1, 4> - } - - $i = 5; - while ($i-- > 0) { - assertType('int<0, max>', $i); // should improved to be int<0, 4> - } - - $i = 5; - while (--$i > 0) { - assertType('int<1, max>', $i); // should improved to be int<1, 4> - } -}; - - -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', $a * $b); - assertType('int', $b * $c); - assertType('int', $a * $b * $c); -}; diff --git a/tests/PHPStan/Analyser/data/intersection-static.php b/tests/PHPStan/Analyser/data/intersection-static.php deleted file mode 100644 index 425d0f3dbe..0000000000 --- a/tests/PHPStan/Analyser/data/intersection-static.php +++ /dev/null @@ -1,53 +0,0 @@ -returnStatic()); - } - - /** - * @param Foo&Baz $intersection - */ - public function doBar($intersection) - { - assertType('IntersectionStatic\Baz&IntersectionStatic\Foo', $intersection); - assertType('IntersectionStatic\Baz&IntersectionStatic\Foo', $intersection->returnStatic()); - } - -} diff --git a/tests/PHPStan/Analyser/data/is-numeric.php b/tests/PHPStan/Analyser/data/is-numeric.php deleted file mode 100644 index d5d84d2d37..0000000000 --- a/tests/PHPStan/Analyser/data/is-numeric.php +++ /dev/null @@ -1,9 +0,0 @@ - 1, 'b' => 2, ]; - } elseif (rand(0, 11) === 0) { + } elseif (rand(0, 10) === 0) { $array = [ 'a' => 2, ]; - } elseif (rand(0, 12) === 0) { + } elseif (rand(0, 10) === 0) { $array = [ 'a' => 3, 'b' => 3, 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 8ebe474d61..0000000000 --- a/tests/PHPStan/Analyser/data/iterator_to_array.php +++ /dev/null @@ -1,18 +0,0 @@ - $ints - */ - public function doFoo(Traversable $ints) - { - assertType('array', iterator_to_array($ints)); - } -} diff --git a/tests/PHPStan/Analyser/data/ldap-exop-passwd.php b/tests/PHPStan/Analyser/data/ldap-exop-passwd.php new file mode 100644 index 0000000000..f82062765e --- /dev/null +++ b/tests/PHPStan/Analyser/data/ldap-exop-passwd.php @@ -0,0 +1,16 @@ +', $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('array', $list); - } - - - public function withMixedType(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - $list[] = new \stdClass(); - assertType('array', $list); - } - - public function withObjectType(): void - { - /** @var list<\DateTime> $list */ - $list = []; - $list[] = new \DateTime(); - assertType('array', $list); - } - - /** @return list */ - public function withScalarGoodContent(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list[] = true; - assertType('array', $list); - } - - public function withNumericKey(): void - { - /** @var list $list */ - $list = []; - $list[] = '1'; - $list['1'] = true; - assertType('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('array', $list2); - } - -} diff --git a/tests/PHPStan/Analyser/data/loose-const-comparison-php7.php b/tests/PHPStan/Analyser/data/loose-const-comparison-php7.php new file mode 100644 index 0000000000..9a0c544df1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/loose-const-comparison-php7.php @@ -0,0 +1,20 @@ += 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/method-phpDocs-inheritdoc-without-curly-braces.php b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php index 0570801ba5..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 { /** @@ -25,7 +25,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -43,9 +42,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); diff --git a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc.php b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc.php index 3aa61cb11c..5c4ddbef6d 100644 --- a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc.php +++ b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc.php @@ -25,7 +25,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -43,9 +42,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-implicitInheritance.php b/tests/PHPStan/Analyser/data/methodPhpDocs-implicitInheritance.php index b778e5ef06..cedd25b75e 100644 --- a/tests/PHPStan/Analyser/data/methodPhpDocs-implicitInheritance.php +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-implicitInheritance.php @@ -22,7 +22,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -40,9 +39,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); 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/methodPhpDocs-phpstanPrefix.php b/tests/PHPStan/Analyser/data/methodPhpDocs-phpstanPrefix.php index 49febde563..32b65d1167 100644 --- a/tests/PHPStan/Analyser/data/methodPhpDocs-phpstanPrefix.php +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-phpstanPrefix.php @@ -67,7 +67,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -85,9 +84,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-psalmPrefix.php b/tests/PHPStan/Analyser/data/methodPhpDocs-psalmPrefix.php index 723504866b..b168c05e4d 100644 --- a/tests/PHPStan/Analyser/data/methodPhpDocs-psalmPrefix.php +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-psalmPrefix.php @@ -67,7 +67,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -85,9 +84,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-trait-defined.php b/tests/PHPStan/Analyser/data/methodPhpDocs-trait-defined.php index fe2fbbe6ea..afe0e75bc1 100644 --- a/tests/PHPStan/Analyser/data/methodPhpDocs-trait-defined.php +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-trait-defined.php @@ -59,7 +59,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -77,9 +76,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs.php b/tests/PHPStan/Analyser/data/methodPhpDocs.php index 5325e5a257..5a6b301c2d 100644 --- a/tests/PHPStan/Analyser/data/methodPhpDocs.php +++ b/tests/PHPStan/Analyser/data/methodPhpDocs.php @@ -67,7 +67,6 @@ public function doFoo( $objectUsed, $nullableInteger, $nullableObject, - $anotherNullableObject = null, $selfType, $staticType, $nullType, @@ -85,9 +84,10 @@ public function doFoo( bool $boolTrue, bool $boolFalse, bool $trueBoolean, - $parameterWithDefaultValueFalse = false, $objectWithoutNativeTypehint, - object $objectWithNativeTypehint + object $objectWithNativeTypehint, + $parameterWithDefaultValueFalse = false, + $anotherNullableObject = null ) { $parent = new FooParent(); diff --git a/tests/PHPStan/Analyser/data/misleading-types-without-namespace.php b/tests/PHPStan/Analyser/data/misleading-types-without-namespace.php index 75eec8031e..25afdac432 100644 --- a/tests/PHPStan/Analyser/data/misleading-types-without-namespace.php +++ b/tests/PHPStan/Analyser/data/misleading-types-without-namespace.php @@ -3,12 +3,12 @@ class FooClassForNodeScopeResolverTestingWithoutNamespace { - public function misleadingBoolReturnType(): boolean + public function misleadingBoolReturnType(): \boolean { } - public function misleadingIntReturnType(): integer + public function misleadingIntReturnType(): \integer { } diff --git a/tests/PHPStan/Analyser/data/misleading-types.php b/tests/PHPStan/Analyser/data/misleading-types.php index e5a40ddd81..1da194f288 100644 --- a/tests/PHPStan/Analyser/data/misleading-types.php +++ b/tests/PHPStan/Analyser/data/misleading-types.php @@ -5,12 +5,12 @@ class Foo { - public function misleadingBoolReturnType(): boolean + public function misleadingBoolReturnType(): \MisleadingTypes\boolean { } - public function misleadingIntReturnType(): integer + public function misleadingIntReturnType(): \MisleadingTypes\integer { } diff --git a/tests/PHPStan/Analyser/data/mixed-typehint.php b/tests/PHPStan/Analyser/data/mixed-typehint.php deleted file mode 100644 index 0d5fbefead..0000000000 --- a/tests/PHPStan/Analyser/data/mixed-typehint.php +++ /dev/null @@ -1,34 +0,0 @@ -doBar()); - } - - public function doBar(): mixed - { - - } - -} - -function doFoo(mixed $foo) -{ - assertType('mixed', $foo); -} - -function (mixed $foo) { - assertType('mixed', $foo); - $f = function (): mixed { - - }; - assertType('mixed', $f()); -}; diff --git a/tests/PHPStan/Analyser/data/native-types.php b/tests/PHPStan/Analyser/data/native-types.php deleted file mode 100644 index 8159013f2f..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('array', $array); - assertNativeType('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::class, $e); - assertNativeType(\Exception::class, $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('array', $array); - assertNativeType('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('DateTimeInterface~DateTimeImmutable', $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/nested-namespaces.php b/tests/PHPStan/Analyser/data/nested-namespaces.php index 0f8bb769f4..ff8818d32c 100644 --- a/tests/PHPStan/Analyser/data/nested-namespaces.php +++ b/tests/PHPStan/Analyser/data/nested-namespaces.php @@ -17,5 +17,21 @@ public function __construct(boo $boo, baz $baz) { $this->boo = $boo; $this->baz = $baz; } + + /** + * @return boo + */ + public function getBoo(): boo + { + return $this->boo; + } + + /** + * @return mixed + */ + public function getBaz() + { + return $this->baz; + } } } diff --git a/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php new file mode 100644 index 0000000000..c46e1b2a91 --- /dev/null +++ b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php @@ -0,0 +1,12 @@ += 8.1 + +namespace NewInInitializers; + +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/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/php74_functions.php b/tests/PHPStan/Analyser/data/php74_functions.php new file mode 100644 index 0000000000..62b2e4b910 --- /dev/null +++ b/tests/PHPStan/Analyser/data/php74_functions.php @@ -0,0 +1,33 @@ + $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-32bit.php b/tests/PHPStan/Analyser/data/predefined-constants-32bit.php new file mode 100644 index 0000000000..8d0fe24eaf --- /dev/null +++ b/tests/PHPStan/Analyser/data/predefined-constants-32bit.php @@ -0,0 +1,7 @@ + + * @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-assign-intersection-static-type-bug.php b/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php index 7b66441340..ff67a805f3 100644 --- a/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php +++ b/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php @@ -13,6 +13,14 @@ public function __construct(string $foo) $this->foo = $foo; } + + /** + * @return string + */ + public function getFoo(): string + { + return $this->foo; + } } class Frontend extends Base diff --git a/tests/PHPStan/Analyser/data/property-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 +', random_int($min, 20)); -}; - -function (int $min) { - \assert($min <= 0); - assertType('int', random_int($min, 20)); -}; - -function (int $max) { - \assert($min >= 0); - assertType('int<0, max>', random_int(0, $max)); -}; - -function (int $i) { - assertType('int', random_int($i, $i)); -}; - -assertType('0', random_int(0, 0)); -assertType('int', random_int(PHP_INT_MIN, PHP_INT_MAX)); -assertType('int<0, max>', random_int(0, PHP_INT_MAX)); -assertType('int', random_int(PHP_INT_MIN, 0)); -assertType('int<-1, 1>', random_int(-1, 1)); -assertType('int<0, 30>', random_int(0, random_int(0, 30))); -assertType('int<0, 100>', random_int(random_int(0, 10), 100)); - -assertType('*NEVER*', random_int(10, 1)); -assertType('*NEVER*', random_int(2, random_int(0, 1))); -assertType('int<0, 1>', random_int(0, random_int(0, 1))); -assertType('*NEVER*', random_int(random_int(0, 1), -1)); -assertType('int<0, 1>', random_int(random_int(0, 1), 1)); - -assertType('int<-5, 5>', random_int(random_int(-5, 0), random_int(0, 5))); -assertType('int', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); diff --git a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php deleted file mode 100644 index c723335076..0000000000 --- a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php +++ /dev/null @@ -1,14 +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 @@ +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 @@ +prop; + } + } if (rand(0, 0)) { @@ -32,5 +40,37 @@ public function doFoo(): void echo self::FOO_CONST; } + /** + * @return int + */ + public function getProp() + { + return $this->prop; + } + + /** + * @param int $prop + */ + public function setProp($prop): void + { + $this->prop = $prop; + } + + /** + * @return int + */ + public function getProp2() + { + return $this->prop2; + } + + /** + * @param int $prop2 + */ + public function setProp2($prop2): void + { + $this->prop2 = $prop2; + } + } } diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-1-false.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-false.php new file mode 100644 index 0000000000..2e4d900db3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-false.php @@ -0,0 +1,14 @@ +assertString($foo); +$test = \PHPStan\Tests\AssertionClass::assertInt($bar); + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-1-null.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-null.php new file mode 100644 index 0000000000..5a9326d3f4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-null.php @@ -0,0 +1,14 @@ +assertString($foo); +$test = \PHPStan\Tests\AssertionClass::assertInt($bar); + +assertType('string', $foo); +assertType('int', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-1-true.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-true.php new file mode 100644 index 0000000000..5a9326d3f4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-true.php @@ -0,0 +1,14 @@ +assertString($foo); +$test = \PHPStan\Tests\AssertionClass::assertInt($bar); + +assertType('string', $foo); +assertType('int', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php new file mode 100644 index 0000000000..8016cee235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php @@ -0,0 +1,14 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { +} + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php new file mode 100644 index 0000000000..8016cee235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php @@ -0,0 +1,14 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { +} + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-true.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-true.php new file mode 100644 index 0000000000..8016cee235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-true.php @@ -0,0 +1,14 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { +} + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-3-false.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-false.php new file mode 100644 index 0000000000..af253cc5f8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-false.php @@ -0,0 +1,13 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { + assertType('string', $foo); + assertType('int', $bar); +} diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-3-null.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-null.php new file mode 100644 index 0000000000..af253cc5f8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-null.php @@ -0,0 +1,13 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { + assertType('string', $foo); + assertType('int', $bar); +} diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-3-true.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-true.php new file mode 100644 index 0000000000..92e26fdccd --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-true.php @@ -0,0 +1,13 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { + assertType('string|null', $foo); + assertType('int|null', $bar); +} diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions.php b/tests/PHPStan/Analyser/data/type-specifying-extensions.php deleted file mode 100644 index 512fd10f87..0000000000 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions.php +++ /dev/null @@ -1,11 +0,0 @@ -assertString($foo); -$test = \PHPStan\Tests\AssertionClass::assertInt($bar); - -die; diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions2.php b/tests/PHPStan/Analyser/data/type-specifying-extensions2.php deleted file mode 100644 index bda2e87e55..0000000000 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions2.php +++ /dev/null @@ -1,11 +0,0 @@ -assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { -} - -die; diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions3.php b/tests/PHPStan/Analyser/data/type-specifying-extensions3.php deleted file mode 100644 index 37a1033c2d..0000000000 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions3.php +++ /dev/null @@ -1,10 +0,0 @@ -assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { - die; -} diff --git a/tests/PHPStan/Analyser/data/union-intersection.php b/tests/PHPStan/Analyser/data/union-intersection.php index 0e0edc880a..03266cd2c8 100644 --- a/tests/PHPStan/Analyser/data/union-intersection.php +++ b/tests/PHPStan/Analyser/data/union-intersection.php @@ -5,6 +5,7 @@ class WithFoo { + /** @var 1 */ const FOO_CONSTANT = 1; /** @var Foo */ @@ -25,7 +26,10 @@ public static function doStaticFoo(): Foo class WithFooAndBar { + /** @var 1 */ const FOO_CONSTANT = 1; + + /** @var 1 */ const BAR_CONSTANT = 1; /** @var AnotherFoo */ @@ -59,7 +63,10 @@ public static function doStaticBar(): Bar interface WithFooAndBarInterface { + /** @var 1 */ const FOO_CONSTANT = 1; + + /** @var 1 */ const BAR_CONSTANT = 1; public function doFoo(): AnotherFoo; @@ -80,6 +87,7 @@ interface SomeInterface class Dolor { + /** @var array{1, 2, 3} */ const PARENT_CONSTANT = [1, 2, 3]; } diff --git a/tests/PHPStan/Analyser/data/unknown-mixed-type.php b/tests/PHPStan/Analyser/data/unknown-mixed-type.php new file mode 100644 index 0000000000..d603b6d3ef --- /dev/null +++ b/tests/PHPStan/Analyser/data/unknown-mixed-type.php @@ -0,0 +1,13 @@ +pipeInto(User::class); +}; diff --git a/tests/PHPStan/Analyser/data/while-loop-variables.php b/tests/PHPStan/Analyser/data/while-loop-variables.php index 792d7a6e33..a23194ab82 100644 --- a/tests/PHPStan/Analyser/data/while-loop-variables.php +++ b/tests/PHPStan/Analyser/data/while-loop-variables.php @@ -7,7 +7,7 @@ function () { $i = 0; $nullableVal = null; $falseOrObject = false; - while ($val = fetch() && $i++ < 10) { + while (($val = fetch()) && $i++ < 10) { 'begin'; $foo = new Foo(); 'afterAssign'; 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 new file mode 100644 index 0000000000..e80f2018a0 --- /dev/null +++ b/tests/PHPStan/Analyser/dynamic-return-type.neon @@ -0,0 +1,41 @@ +services: + - + class: PHPStan\Tests\GetByPrimaryDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\OffsetGetDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\CreateManagerForEntityDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Tests\ConstructDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Tests\ConstructWithoutConstructor + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Tests\GetSelfDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\FooGetSelf + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + 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 new file mode 100644 index 0000000000..905063b2f6 --- /dev/null +++ b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon @@ -0,0 +1,21 @@ +services: + - + class: DynamicMethodThrowTypeExtension\MethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + + - + 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/functions.php b/tests/PHPStan/Analyser/functions.php deleted file mode 100644 index aa17f0e9f8..0000000000 --- a/tests/PHPStan/Analyser/functions.php +++ /dev/null @@ -1,26 +0,0 @@ -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/nsrt/array-column-php7.php b/tests/PHPStan/Analyser/nsrt/array-column-php7.php new file mode 100644 index 0000000000..5d8018f599 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-column-php7.php @@ -0,0 +1,22 @@ + $array */ + public function testConstantArray1(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-column-php8.php b/tests/PHPStan/Analyser/nsrt/array-column-php8.php new file mode 100644 index 0000000000..7bbdccbe45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-column-php8.php @@ -0,0 +1,22 @@ += 8.0 + +namespace ArrayColumn\Php8; + +use function PHPStan\Testing\assertType; + +class ArrayColumnPhp7Test +{ + + /** @param array $array */ + public function testConstantArray1(array $array): void + { + assertType('array<*NEVER*, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/array-destructuring-types.php b/tests/PHPStan/Analyser/nsrt/array-destructuring-types.php new file mode 100644 index 0000000000..7c310cf5d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-destructuring-types.php @@ -0,0 +1,70 @@ +foo] = [1]; + assertType('1', $this->foo); + } + + public function doBar() + { + foreach ([1, 2, 3] as $key => $this->foo) { + assertType('0|1|2', $key); + assertType('1|2|3', $this->foo); + } + } + + public function doBaz() + { + foreach ([[1], [2], [3]] as $key => [$this->foo]) { + assertType('0|1|2', $key); + assertType('1|2|3', $this->foo); + } + } + + public function doLorem() + { + foreach ([[1]] as $key => [$this->foo]) { + assertType('0', $key); + assertType('1', $this->foo); + } + } + +} + +class Bar +{ + + public function doFoo() + { + + $matrix = $this->preprocessOpeningHours(); + if ($matrix === []) { + return null; + } + + /** @var string[][] $matrix */ + $matrix[] = end($matrix); + + assertType('array>', $matrix); + } + + /** + * @return string[][] + */ + private function preprocessOpeningHours(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/array-filter-arrow-functions.php b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php new file mode 100644 index 0000000000..4ad761fc71 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php @@ -0,0 +1,43 @@ + $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, static fn($item): bool => is_string($item)); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, static fn($key): bool => is_string($key), ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, static fn($item, $key): bool => is_string($item) && is_string($key), ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + * @param array $map4 + */ +function filtersString(array $map1, array $map2, array $map3, array $map4): void +{ + $filtered1 = array_filter($map1, static fn($item): bool => is_string($item)); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, static fn($key): bool => is_string($key), ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, static fn($item, $key): bool => is_string($item) && is_string($key), ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); + + $filtered4 = array_filter($map4, static fn($item): bool => is_string($item), ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered4); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-filter-callables.php b/tests/PHPStan/Analyser/nsrt/array-filter-callables.php new file mode 100644 index 0000000000..3ac42c8790 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter-callables.php @@ -0,0 +1,43 @@ + $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, static function ($item): bool { return is_string($item); }); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, static function ($key): bool { return is_string($key); }, ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, static function ($item, $key): bool { return is_string($item) && is_string($key); }, ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + * @param array $map4 + */ +function filtersString(array $map1, array $map2, array $map3, array $map4): void +{ + $filtered1 = array_filter($map1, static function ($item): bool { return is_string($item); }); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, static function ($key): bool { return is_string($key); }, ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, static function ($item, $key): bool { return is_string($item) && is_string($key); }, ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); + + $filtered4 = array_filter($map4, static function ($item): bool { return is_string($item); }, ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered4); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-filter-constant.php b/tests/PHPStan/Analyser/nsrt/array-filter-constant.php new file mode 100644 index 0000000000..77bff18b84 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter-constant.php @@ -0,0 +1,32 @@ +|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/nsrt/array-filter-string-callables.php b/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php new file mode 100644 index 0000000000..f6e0b6c65e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php @@ -0,0 +1,90 @@ + $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, 'is_string'); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, 'is_string', ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, 'is_string', ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + */ +function filtersString(array $map1, array $map2, array $map3): void +{ + $filtered1 = array_filter($map1, 'is_string'); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, 'is_string', ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, 'is_string', ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); +} + +/** + * @param array $list1 + */ +function nonCallableStringIsIgnored(array $list1): void +{ + $filtered1 = array_filter($list1, 'foo'); + assertType('array', $filtered1); +} + +/** + * @param array $map1 + * @param array $map2 + */ +function nonBuiltInFunctionsAreNotSupportedYetAndThereforeIgnored(array $map1, array $map2): void +{ + $filtered1 = array_filter($map1, '\ArrayFilter\isString'); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, '\ArrayFilter\Filters::isString'); + assertType('array', $filtered2); +} + +/** + * @param mixed $value + */ +function isString($value): bool +{ + return is_string($value); +} + +class Filters { + /** + * @param mixed $value + */ + public static function isString($value): bool + { + return is_string($value); + } +} + +function unionOfCallableStrings(): void +{ + $func = rand(0, 1) === 1 ? 'is_string' : 'is_int'; + $list = [ + 1, + 2, + 'foo', + ]; + assertType("array{1, 2}|array{2: 'foo'}", array_filter($list, $func)); +} diff --git a/tests/PHPStan/Analyser/nsrt/array-filter.php b/tests/PHPStan/Analyser/nsrt/array-filter.php new file mode 100644 index 0000000000..682e117812 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-filter.php @@ -0,0 +1,42 @@ + $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 88% rename from tests/PHPStan/Analyser/data/array-key.php rename to tests/PHPStan/Analyser/nsrt/array-key.php index 8903b2d173..a4b7b36370 100644 --- a/tests/PHPStan/Analyser/data/array-key.php +++ b/tests/PHPStan/Analyser/nsrt/array-key.php @@ -2,7 +2,7 @@ namespace ArrayKey; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { diff --git a/tests/PHPStan/Analyser/nsrt/array-map-closure.php b/tests/PHPStan/Analyser/nsrt/array-map-closure.php new file mode 100644 index 0000000000..d09a0d294c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-map-closure.php @@ -0,0 +1,40 @@ + $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/nsrt/array-next.php b/tests/PHPStan/Analyser/nsrt/array-next.php new file mode 100644 index 0000000000..97dc582e49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-next.php @@ -0,0 +1,104 @@ + $a + */ + public function doBaz(array $a) + { + assertType('string|false', next($a)); + } + +} + +class Foo2 +{ + + public function doFoo() + { + $array = []; + assertType('false', prev($array)); + } + + /** + * @param int[] $a + */ + public function doBar(array $a) + { + assertType('int|false', prev($a)); + } + + /** + * @param non-empty-array $a + */ + public function doBaz(array $a) + { + assertType('string|false', prev($a)); + } + +} + +interface HttpClientPoolItem +{ + public function isDisabled(): bool; +} + +final class RoundRobinClientPool +{ + /** + * @var HttpClientPoolItem[] + */ + protected $clientPool = []; + + protected function chooseHttpClient(): HttpClientPoolItem + { + $last = current($this->clientPool); + assertType(HttpClientPoolItem::class . '|false', $last); + + do { + $client = next($this->clientPool); + assertType(HttpClientPoolItem::class . '|false', $client); + + if (false === $client) { + $client = reset($this->clientPool); + assertType(HttpClientPoolItem::class . '|false', $client); + + if (false === $client) { + throw new \Exception(); + } + + assertType(HttpClientPoolItem::class, $client); + } + + assertType(HttpClientPoolItem::class, $client); + + // Case when there is only one and the last one has been disabled + if ($last === $client) { + assertType(HttpClientPoolItem::class, $client); + throw new \Exception(); + } + } while ($client->isDisabled()); + + return $client; + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/array-plus.php b/tests/PHPStan/Analyser/nsrt/array-plus.php new file mode 100644 index 0000000000..b6d8b99e2d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-plus.php @@ -0,0 +1,20 @@ + $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 @@ + $dollar + */ + public function doFoo(array $slash, array $dollar): void + { + assertType('array{namespace/key: string}', $slash); + assertType('array', $dollar); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-shift.php b/tests/PHPStan/Analyser/nsrt/array-shift.php new file mode 100644 index 0000000000..2d8ef21d7e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-shift.php @@ -0,0 +1,95 @@ + $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/nsrt/array-typehint-without-null-in-phpdoc.php b/tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php new file mode 100644 index 0000000000..aafff14030 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php @@ -0,0 +1,31 @@ +|null', $this->doFoo()); + } + + /** + * @param string[] $a + */ + public function doBaz(?array $a): void + { + assertType('array|null', $a); + } + +} 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 83% rename from tests/PHPStan/Analyser/data/arrow-function-return-type.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php index 6cd49d1a74..ff4055dc3e 100644 --- a/tests/PHPStan/Analyser/data/arrow-function-return-type.php +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php @@ -2,7 +2,7 @@ namespace ArrowFunctionReturnTypeInference; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; function (int $i): void { $fn = fn () => $i; diff --git a/tests/PHPStan/Analyser/nsrt/arrow-function-types.php b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php new file mode 100644 index 0000000000..acb8f74ee4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php @@ -0,0 +1,44 @@ + */ + private $arrayShapes; + + public function doFoo(): void + { + array_map(fn(array $a): array => assertType('array{foo: string, bar: int}', $a), $this->arrayShapes); + $a = array_map(fn(array $a) => $a, $this->arrayShapes); + assertType('array', $a); + + array_map(fn($b) => assertType('array{foo: string, bar: int}', $b), $this->arrayShapes); + $b = array_map(fn($b) => $b['foo'], $this->arrayShapes); + assertType('array', $b); + } + + public function doBar(): void + { + usort($this->arrayShapes, fn(array $a, array $b): int => assertType('array{foo: string, bar: int}', $a)); + } + + public function doBar2(): void + { + usort($this->arrayShapes, fn (array $a, array $b): int => assertType('array{foo: string, bar: int}', $b)); + } + + public function doBaz(): void + { + usort($this->arrayShapes, fn ($a, $b): int => assertType('array{foo: string, bar: int}', $a)); + } + + public function doBaz2(): void + { + usort($this->arrayShapes, fn ($a, $b): int => assertType('array{foo: string, bar: int}', $b)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/assert-class-type.php b/tests/PHPStan/Analyser/nsrt/assert-class-type.php new file mode 100644 index 0000000000..5bb97f4fce --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-class-type.php @@ -0,0 +1,42 @@ +value = $value; + } + + /** + * @template K + * @param K $data + * @phpstan-assert T $data + */ + public function assert($data): void + { + if ($data !== $this->value) { + throw new Exception(); + } + } +} + +function () { + $a = new HelloWorld(123); + assertType('AssertClassType\\HelloWorld', $a); + + $b = $_GET['value']; + $a->assert($b); + + assertType('int', $b); +}; diff --git a/tests/PHPStan/Analyser/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 @@ +', $array); + } + + public function doBar(int $i, int $j) + { + $array = []; + + $array[$i][$j]['bar'] = 1; + $array[$i][$j]['baz'] = 2; + + echo $array[$i][$j]['bar']; + echo $array[$i][$j]['baz']; + + assertType('non-empty-array>', $array); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/asymmetric-properties.php b/tests/PHPStan/Analyser/nsrt/asymmetric-properties.php new file mode 100644 index 0000000000..a5a69a5341 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/asymmetric-properties.php @@ -0,0 +1,29 @@ +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/nsrt/bug-1014.php b/tests/PHPStan/Analyser/nsrt/bug-1014.php new file mode 100644 index 0000000000..d146c3341f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1014.php @@ -0,0 +1,20 @@ +", $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/nsrt/bug-1157.php b/tests/PHPStan/Analyser/nsrt/bug-1157.php new file mode 100644 index 0000000000..07ac315887 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1157.php @@ -0,0 +1,25 @@ +a; + } +} + +function (HelloWorld $class): void { + if ($class->getA()) { + assertType(DateTimeInterface::class, $class->getA()); + } +}; 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/nsrt/bug-1209.php b/tests/PHPStan/Analyser/nsrt/bug-1209.php new file mode 100644 index 0000000000..4b4ce770d1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1209.php @@ -0,0 +1,34 @@ +', $value); + } + } + + /** + * @param mixed[]|string $value + */ + public function sayHello2($value): void + { + $isArray = is_array($value); + $value = 123; + assertType('123', $value); + if ($isArray) { + assertType('123', $value); + } + + assertType('123', $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 85% rename from tests/PHPStan/Analyser/data/bug-1216.php rename to tests/PHPStan/Analyser/nsrt/bug-1216.php index 0f1aec142b..7c0beae95d 100644 --- a/tests/PHPStan/Analyser/data/bug-1216.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1216.php @@ -2,7 +2,8 @@ namespace Bug1216; -use function PHPStan\Analyser\assertType; +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/nsrt/bug-1219.php b/tests/PHPStan/Analyser/nsrt/bug-1219.php new file mode 100644 index 0000000000..edde8c456a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1219.php @@ -0,0 +1,29 @@ +getCode()) { + case 1: + return 0; + default: + return -1; + } + } + + assertVariableCertainty(TrinaryLogic::createYes(), $var); + + return $var; + } + +} 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/nsrt/bug-1283.php b/tests/PHPStan/Analyser/nsrt/bug-1283.php new file mode 100644 index 0000000000..058c739bb9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1283.php @@ -0,0 +1,27 @@ += 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/nsrt/bug-1511.php b/tests/PHPStan/Analyser/nsrt/bug-1511.php new file mode 100644 index 0000000000..a418374fe5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1511.php @@ -0,0 +1,49 @@ +message = $message; + } + +} + +class Serializer +{ + + public function serialize(): void + { + + } + +} + +class Controller +{ + public function prepareResponse($date): Response + { + $serializer = new Serializer(); + $response = null; + try { + $serializer->serialize(); + // do something with response object e.g. serialize some entity + $response = new Response("Success"); + } catch (\Throwable $e) { + $response = new Response("Error"); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $response); + assertType(Response::class, $response); + return $response; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1516.php b/tests/PHPStan/Analyser/nsrt/bug-1516.php new file mode 100644 index 0000000000..d62795a24b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1516.php @@ -0,0 +1,37 @@ + 'barr', + 'ftt' => [] + ]; + + foreach ($a as $k => $b) { + $str = 'toto'; + assertType('\'toto\'|array{}', $out[$k]); + + if (is_array($b)) { + // $out[$k] is redefined there before the array_merge + assertType('\'toto\'|array{}', $out[$k]); + $out[$k] = []; + assertType('array{}', $out[$k]); + $out[$k] = array_merge($out[$k], []); + assertType('array{}', $out[$k]); + + } else { + // I think phpstan takes this definition as a string and takes no account of the foreach + $out[$k] = $str; + assertType('\'toto\'', $out[$k]); + } + } + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-1597.php b/tests/PHPStan/Analyser/nsrt/bug-1597.php new file mode 100644 index 0000000000..def66f078e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1597.php @@ -0,0 +1,16 @@ +children)) { + case 0: + assertType('array{}', $this->children); + break; + case 1: + assertType('non-empty-array<' . self::class . '>', $this->children); + assertType(self::class, reset($this->children)); + break; + default: + assertType('non-empty-array<' . self::class . '>', $this->children); + break; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1865.php b/tests/PHPStan/Analyser/nsrt/bug-1865.php new file mode 100644 index 0000000000..b4e3a39845 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1865.php @@ -0,0 +1,23 @@ + $this->getArrayOrNull(), + 'b' => $this->getArrayOrNull(), + ]; + assertType('array{a: array|null, b: array|null}', $arr); + + $cond = isset($arr['a']) && isset($arr['b']); + assertType('bool', $cond); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1945.php b/tests/PHPStan/Analyser/nsrt/bug-1945.php new file mode 100644 index 0000000000..8776a03c52 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1945.php @@ -0,0 +1,106 @@ +, 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/nsrt/bug-2003.php b/tests/PHPStan/Analyser/nsrt/bug-2003.php new file mode 100644 index 0000000000..125b1a2a16 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2003.php @@ -0,0 +1,25 @@ +getFoos(); + assertType('array', $foos); + $foos[0] = null; + + assertType('null', $foos[0]); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); + } + + /** @return self[] */ + public function getFooBars(): array + { + return []; + } + + public function doBars(): void + { + $foos = $this->getFooBars(); + assertType('array', $foos); + $foos[0] = null; + + assertType('null', $foos[0]); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2142.php b/tests/PHPStan/Analyser/nsrt/bug-2142.php new file mode 100644 index 0000000000..64e39ecd25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2142.php @@ -0,0 +1,99 @@ + 0) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo2(array $arr): void + { + if (count($arr) != 0) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo3(array $arr): void + { + if (count($arr) == 1) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo4(array $arr): void + { + if ($arr != []) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo5(array $arr): void + { + if (sizeof($arr) !== 0) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo6(array $arr): void + { + if (count($arr) !== 0) { + assertType('non-empty-array', $arr); + } + } + + + /** + * @param string[] $arr + */ + function doFoo7(array $arr): void + { + if (!empty($arr)) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo8(array $arr): void + { + if (count($arr) === 1) { + assertType('non-empty-array', $arr); + } + } + + + /** + * @param string[] $arr + */ + function doFoo9(array $arr): void + { + if ($arr !== []) { + assertType('non-empty-array', $arr); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2231.php b/tests/PHPStan/Analyser/nsrt/bug-2231.php new file mode 100644 index 0000000000..9afaf3d3f5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2231.php @@ -0,0 +1,39 @@ + "a", + 'a2' => "b", + 'a3' => "c", + 'a4' => [ + 'name' => "dsfs", + 'version' => "fdsfs", + ], + ]; + + if (rand(0, 1)) { + $data['b1'] = "hello"; + } + + if (rand(0, 1)) { + $data['b2'] = "hello"; + } + + if (rand(0, 1)) { + $data['b3'] = "hello"; + } + + if (rand(0, 1)) { + $data['b4'] = "goodbye"; + } + + if (rand(0, 1)) { + $data['b5'] = "env"; + } + + assertType('array{a1: \'a\', a2: \'b\', a3: \'c\', a4: array{name: \'dsfs\', version: \'fdsfs\'}, b1?: \'hello\', b2?: \'hello\', b3?: \'hello\', b4?: \'goodbye\', b5?: \'env\'}', $data); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2288.php b/tests/PHPStan/Analyser/nsrt/bug-2288.php new file mode 100644 index 0000000000..e0e10a986e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2288.php @@ -0,0 +1,24 @@ +test()) { + assertType(\DateTimeImmutable::class, $test->test()); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2378.php b/tests/PHPStan/Analyser/nsrt/bug-2378.php new file mode 100644 index 0000000000..a05de0f302 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2378.php @@ -0,0 +1,23 @@ +', range($s, $s)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2413.php b/tests/PHPStan/Analyser/nsrt/bug-2413.php new file mode 100644 index 0000000000..a9ea8ccd16 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2413.php @@ -0,0 +1,33 @@ +getTest())) { + // Cannot access property $field on TestClass|null + assertType(TestClass::class, $test); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2420.php b/tests/PHPStan/Analyser/nsrt/bug-2420.php new file mode 100644 index 0000000000..8b3d5b4809 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2420.php @@ -0,0 +1,33 @@ + false, + 1 => false, + ]; + + public function sayHello(int $key): void + { + $config = self::CONFIG[$key] ?? true; + assertType('bool', $config); + } +} + +class HelloWorld2 +{ + const CONFIG = [ + 0 => ['foo' => false], + 1 => ['foo' => false], + ]; + + public function sayHello(int $key): void + { + $config = self::CONFIG[$key]['foo'] ?? true; + assertType('bool', $config); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2443.php b/tests/PHPStan/Analyser/nsrt/bug-2443.php similarity index 87% rename from tests/PHPStan/Analyser/data/bug-2443.php rename to tests/PHPStan/Analyser/nsrt/bug-2443.php index ceacdfb47f..7ab56ef332 100644 --- a/tests/PHPStan/Analyser/data/bug-2443.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2443.php @@ -2,7 +2,7 @@ namespace Analyser\Bug2443; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; /** * @param array $a 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/nsrt/bug-2539.php b/tests/PHPStan/Analyser/nsrt/bug-2539.php new file mode 100644 index 0000000000..34bc26f5a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2539.php @@ -0,0 +1,33 @@ + $array + * @param non-empty-array $nonEmptyArray + */ + public function doFoo( + array $array, + array $nonEmptyArray + ): void + { + assertType('int|false', current($array)); + assertType('int', current($nonEmptyArray)); + + assertType('false', current([])); + assertType('1|2|3', current([1, 2, 3])); + + $a = []; + if (rand(0, 1)) { + $a[] = 1; + } + + assertType('1|false', current($a)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2549.php b/tests/PHPStan/Analyser/nsrt/bug-2549.php new file mode 100644 index 0000000000..ad9f6601ea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2549.php @@ -0,0 +1,53 @@ + $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/nsrt/bug-2600-php8.php b/tests/PHPStan/Analyser/nsrt/bug-2600-php8.php new file mode 100644 index 0000000000..4dd75b4250 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2600-php8.php @@ -0,0 +1,88 @@ += 8.0 + +namespace Bug2600Php8; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @param mixed ...$x + */ + public function doFoo($x = null) { + $args = func_get_args(); + assertType('mixed', $x); + assertType('list', $args); + } + + /** + * @param mixed ...$x + */ + public function doBar($x = null) { + assertType('mixed', $x); + } + + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + + /** + * @param mixed ...$x + */ + public function doLorem(...$x) { + assertType('array', $x); + } + + public function doIpsum($x = null) { + $args = func_get_args(); + assertType('mixed', $x); + assertType('list', $args); + } +} + +class Bar +{ + /** + * @param string ...$x + */ + public function doFoo($x = null) { + $args = func_get_args(); + assertType('string|null', $x); + assertType('list', $args); + } + + /** + * @param string ...$x + */ + public function doBar($x = null) { + assertType('string|null', $x); + } + + /** + * @param string $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + + /** + * @param string ...$x + */ + public function doLorem(...$x) { + assertType('array', $x); + } +} + +function foo($x, string ...$y): void +{ + assertType('mixed', $x); + assertType('array', $y); +} + +function ($x, string ...$y): void { + assertType('mixed', $x); + assertType('array', $y); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2600.php b/tests/PHPStan/Analyser/nsrt/bug-2600.php new file mode 100644 index 0000000000..9ab5e49598 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2600.php @@ -0,0 +1,88 @@ +', $args); + } + + /** + * @param mixed ...$x + */ + public function doBar($x = null) { + assertType('mixed', $x); + } + + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('list', $x); + } + + /** + * @param mixed ...$x + */ + public function doLorem(...$x) { + assertType('list', $x); + } + + public function doIpsum($x = null) { + $args = func_get_args(); + assertType('mixed', $x); + assertType('list', $args); + } +} + +class Bar +{ + /** + * @param string ...$x + */ + public function doFoo($x = null) { + $args = func_get_args(); + assertType('string|null', $x); + assertType('list', $args); + } + + /** + * @param string ...$x + */ + public function doBar($x = null) { + assertType('string|null', $x); + } + + /** + * @param string $x + */ + public function doBaz(...$x) { + assertType('list', $x); + } + + /** + * @param string ...$x + */ + public function doLorem(...$x) { + assertType('list', $x); + } +} + +function foo($x, string ...$y): void +{ + assertType('mixed', $x); + assertType('list', $y); +} + +function ($x, string ...$y): void { + assertType('mixed', $x); + assertType('list', $y); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2611.php b/tests/PHPStan/Analyser/nsrt/bug-2611.php new file mode 100644 index 0000000000..6f9df231e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2611.php @@ -0,0 +1,30 @@ +sayHello())){ + assertType('string', $hello); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2648.php b/tests/PHPStan/Analyser/nsrt/bug-2648.php new file mode 100644 index 0000000000..9acaa05026 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2648.php @@ -0,0 +1,53 @@ + 1) { + assertType('int<2, max>', count($list)); + unset($list['fooo']); + assertType("array", $list); + assertType('int<0, max>', count($list)); + } + } + + /** + * @param bool[] $list + */ + public function doBar(array $list): void + { + if (count($list) > 1) { + assertType('int<2, max>', count($list)); + foreach ($list as $key => $item) { + assertType('0|int<2, max>', count($list)); + if ($item === false) { + unset($list[$key]); + assertType('int<0, max>', count($list)); + } + + assertType('int<0, max>', count($list)); + + if (count($list) === 1) { + assertType('1', count($list)); + $list[] = false; + assertType('int<1, max>', count($list)); + break; + } + } + + assertType('int<0, max>', count($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 85% rename from tests/PHPStan/Analyser/data/bug-2676.php rename to tests/PHPStan/Analyser/nsrt/bug-2676.php index 479e275e7e..30723db8a9 100644 --- a/tests/PHPStan/Analyser/data/bug-2676.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2676.php @@ -3,7 +3,7 @@ namespace Bug2676; use DoctrineIntersectionTypeIsSupertypeOf\Collection; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class BankAccount { @@ -38,7 +38,8 @@ function (Wallet $wallet): void $bankAccounts = $wallet->getBankAccountList(); assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable', $bankAccounts); - foreach ($bankAccounts as $bankAccount) { + foreach ($bankAccounts as $key => $bankAccount) { + assertType('mixed', $key); assertType('Bug2676\BankAccount', $bankAccount); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2677.php b/tests/PHPStan/Analyser/nsrt/bug-2677.php new file mode 100644 index 0000000000..22656ae4a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2677.php @@ -0,0 +1,49 @@ + 'a', 'user_id' => 'id1'], + ['group_id' => 'a', 'user_id' => 'id2'], + ['group_id' => 'a', 'user_id' => 'id3'], + ['group_id' => 'b', 'user_id' => 'id4'], + ['group_id' => 'b', 'user_id' => 'id5'], + ['group_id' => 'b', 'user_id' => 'id6'], + ]; + }; + + $orders = $fun(); + + $result = []; + foreach ($orders as $order) { + assertType('bool', isset($result[$order['group_id']]['users'])); + if (isset($result[$order['group_id']]['users'])) { + $result[$order['group_id']]['users'][] = $order['user_id']; + continue; + } + + $result[$order['group_id']] = [ + 'users' => [ + $order['user_id'], + ], + ]; + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2733.php b/tests/PHPStan/Analyser/nsrt/bug-2733.php new file mode 100644 index 0000000000..f18f5053d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2733.php @@ -0,0 +1,24 @@ + */ + 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 75% rename from tests/PHPStan/Analyser/data/bug-2740.php rename to tests/PHPStan/Analyser/nsrt/bug-2740.php index a5cdb70781..2aa2ef14be 100644 --- a/tests/PHPStan/Analyser/data/bug-2740.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2740.php @@ -2,7 +2,7 @@ namespace Bug2740; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; function (Member $member): void { diff --git a/tests/PHPStan/Analyser/nsrt/bug-2750.php b/tests/PHPStan/Analyser/nsrt/bug-2750.php new file mode 100644 index 0000000000..c48c567e25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2750.php @@ -0,0 +1,27 @@ + 0); + assertType('int<1, max>', count($input)); + array_shift($input); + assertType('int<0, max>', count($input)); + + \assert(count($input) > 0); + assertType('int<1, max>', count($input)); + array_pop($input); + assertType('int<0, max>', count($input)); + + \assert(count($input) > 0); + assertType('int<1, max>', count($input)); + array_unshift($input, 'test'); + assertType('int<1, max>', count($input)); + + \assert(count($input) > 0); + assertType('int<1, max>', count($input)); + array_push($input, 'nope'); + assertType('int<1, max>', count($input)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2760.php b/tests/PHPStan/Analyser/nsrt/bug-2760.php new file mode 100644 index 0000000000..72e2e6a516 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2760.php @@ -0,0 +1,24 @@ +', $i); if ($tokens[$i]['code'] !== 1) { assertType('mixed~1', $tokens[$i]['code']); $i++; - assertType('int', $i); + assertType('int<1, max>', $i); assertType('mixed', $tokens[$i]['code']); continue; } assertType('1', $tokens[$i]['code']); $i++; - assertType('int', $i); + assertType('int<1, max>', $i); assertType('mixed', $tokens[$i]['code']); if ($tokens[$i]['code'] !== 2) { assertType('mixed~2', $tokens[$i]['code']); $i++; - assertType('int', $i); + assertType('int<2, max>', $i); continue; } assertType('2', $tokens[$i]['code']); diff --git a/tests/PHPStan/Analyser/data/bug-2850.php b/tests/PHPStan/Analyser/nsrt/bug-2850.php similarity index 91% rename from tests/PHPStan/Analyser/data/bug-2850.php rename to tests/PHPStan/Analyser/nsrt/bug-2850.php index 58873f0faa..76666b52d9 100644 --- a/tests/PHPStan/Analyser/data/bug-2850.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2850.php @@ -2,7 +2,7 @@ namespace Bug2850; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { diff --git a/tests/PHPStan/Analyser/data/bug-2863.php b/tests/PHPStan/Analyser/nsrt/bug-2863.php similarity index 81% rename from tests/PHPStan/Analyser/data/bug-2863.php rename to tests/PHPStan/Analyser/nsrt/bug-2863.php index ece32bdf01..1e81b90d1d 100644 --- a/tests/PHPStan/Analyser/data/bug-2863.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2863.php @@ -2,10 +2,10 @@ namespace Bug2863; -use function PHPStan\Analyser\assertType; +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/nsrt/bug-2869.php b/tests/PHPStan/Analyser/nsrt/bug-2869.php new file mode 100644 index 0000000000..dc6ed20185 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2869.php @@ -0,0 +1,32 @@ + $bar + */ + public function doFoo(array $bar): void + { + if (array_key_exists('foo', $bar)) { + $foobar = isset($bar['foo']); + assertType('bool' ,$foobar); + } + } + + /** + * @param array $bar + */ + public function doBar(array $bar): void + { + if (array_key_exists('foo', $bar)) { + $foobar = isset($bar['foo']); + assertType('true' ,$foobar); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2899.php b/tests/PHPStan/Analyser/nsrt/bug-2899.php new file mode 100644 index 0000000000..557f95cc96 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2899.php @@ -0,0 +1,18 @@ + + */ + 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/nsrt/bug-2927.php b/tests/PHPStan/Analyser/nsrt/bug-2927.php new file mode 100644 index 0000000000..a7f7811398 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2927.php @@ -0,0 +1,35 @@ +getParameters()[0]->getClass(); + if ($paramClass === null or !$paramClass->isSubclassOf(Event::class)) { + return; + } + + /** @var \ReflectionClass $paramClass */ + + assertType('class-string', $paramClass->getName()); + + try { + throw new \Exception(); + } catch (\Exception $e) { + + } + + assertType('class-string', $paramClass->getName()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2945.php b/tests/PHPStan/Analyser/nsrt/bug-2945.php new file mode 100644 index 0000000000..a680906b1f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2945.php @@ -0,0 +1,44 @@ +x = $b->x; + } + } + + /** + * @param \stdClass[] $blocks + * + * @return void + */ + public function doBar(array $blocks){ + foreach($blocks as $b){ + if(!($b instanceof \stdClass)){ + assertType('*NEVER*', $b); + assertNativeType('mixed~stdClass', $b); + throw new \TypeError(); + } + $pk = new \Exception(); + + $x = $b->x; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2969.php b/tests/PHPStan/Analyser/nsrt/bug-2969.php new file mode 100644 index 0000000000..a494cd2c03 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2969.php @@ -0,0 +1,30 @@ +methodWithOccasionalUndocumentedException(); + $done = true; + } finally { + assertType('bool', $done); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2977.php b/tests/PHPStan/Analyser/nsrt/bug-2977.php new file mode 100644 index 0000000000..285fff0d1b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2977.php @@ -0,0 +1,80 @@ +parent; + } + + /** + * {@inheritdoc} + */ + public function getData() + { + return $this->data; + } +} + +class Bar +{ + public function baz(MyInterface $my): string + { + $parent = $my->getParent(); + if (!$parent) { + assertType('null', $parent); + return 'ok'; + } + + assertType(MyInterface::class, $parent); + + return $parent->getData(); + } + + public function baz2(MyInterface $my): string + { + $parent = $my->getParent(); + + $case = $parent; + if (!$case) { + assertType('null', $parent); + return 'ok'; + } + + assertType(MyInterface::class, $parent); + + return $parent->getData(); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2980.php b/tests/PHPStan/Analyser/nsrt/bug-2980.php new file mode 100644 index 0000000000..6592b4f022 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2980.php @@ -0,0 +1,34 @@ +v(); + + // 1. Direct test in IF statement - Correct + if (is_array($v)) { + array_shift($v); + } + + // 2. Direct test in IF (ternary)Correct + print_r(is_array($v) ? array_shift($v) : 'xyz'); + + // 3. Result of test stored into variable - PHPStan thows an eror + $isArray = is_array($v); + if ($isArray) { + assertType('array', $v); + array_shift($v); + } + + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2997.php b/tests/PHPStan/Analyser/nsrt/bug-2997.php new file mode 100644 index 0000000000..0a19cf42f0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2997.php @@ -0,0 +1,9 @@ +Exists); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3004.php b/tests/PHPStan/Analyser/nsrt/bug-3004.php new file mode 100644 index 0000000000..8ae8147345 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3004.php @@ -0,0 +1,25 @@ +getA(); + } catch (\InvalidArgumentException $e) { + $a = 2; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + } + + private function getA(): int + { + throw new \DomainException('test'); + } +} 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 @@ +getUser(); + assertType('Bug3024\A', $elt); + } else { + $u = $elt; + } + } + } + + /** @param U[]|A[] $arr */ + public function bar($arr) : void + { + foreach($arr as $elt) { + if($admin = ($elt instanceof A)) { + assertType('Bug3024\A', $elt); + } else { + $u = $elt; + } + } + } + + /** @param U[]|A[] $arr */ + public function baz($arr) : void + { + foreach($arr as $elt) { + $admin = ($elt instanceof A); + if($admin) { + assertType('Bug3024\A', $elt); + } else { + $u = $elt; + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3044.php b/tests/PHPStan/Analyser/nsrt/bug-3044.php new file mode 100644 index 0000000000..fc3d6e53ff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3044.php @@ -0,0 +1,42 @@ +> + */ + private function getIntArrayFloatArray(): array + { + return [ + 0 => [1.1, 2.2, 3.3], + 1 => [1.1, 2.2, 3.3], + 2 => [1.1, 2.2, 3.3], + ]; + } + + /** + * @return array> + */ + public function invalidType(): void + { + $X = $this->getIntArrayFloatArray(); + $L = $this->getIntArrayFloatArray(); + + $n = 3; + $m = 3; + + for ($k = 0; $k < $m; ++$k) { + for ($j = 0; $j < $n; ++$j) { + for ($i = 0; $i < $k; ++$i) { + $X[$k][$j] -= $X[$i][$j] * $L[$k][$i]; + } + } + } + + assertType('array>', $X); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3106.php b/tests/PHPStan/Analyser/nsrt/bug-3106.php new file mode 100644 index 0000000000..4d2c3d69e3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3106.php @@ -0,0 +1,33 @@ +|null + */ + public function match(string $test): ?array + { + if($test === '') { + return null; + } else { + return [1 => $test]; + } + } + + public function test(): void + { + if( + (is_string($this->expression) === true) && + (is_array($match = $this->match($this->expression)) === true) + ) { + assertType('array', $match); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3126.php b/tests/PHPStan/Analyser/nsrt/bug-3126.php new file mode 100644 index 0000000000..9f675a4239 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3126.php @@ -0,0 +1,37 @@ + $input + */ + function failure(array $input): void { + $results = []; + + foreach ($input as $keyOne => $layerOne) { + assertType('bool', isset($results[$keyOne]['name'])); + if(isset($results[$keyOne]['name']) === false) { + $results[$keyOne]['name'] = $layerOne; + } + } + } + + /** + * @param array $input + */ + function no_failure(array $input): void { + $results = []; + + foreach ($input as $keyOne => $layerOne) { + if(isset($results[$keyOne]) === false) { + $results[$keyOne] = $layerOne; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3132.php b/tests/PHPStan/Analyser/nsrt/bug-3132.php new file mode 100644 index 0000000000..d744dbe9fc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3132.php @@ -0,0 +1,48 @@ + $objects + * + * @return array + */ + function filter(array $objects) : array + { + return array_filter($objects, static function ($key) { + assertType('string', $key); + }, ARRAY_FILTER_USE_KEY); + } + + /** + * @param array $objects + * + * @return array + */ + function bar(array $objects) : array + { + return array_filter($objects, static function ($val) { + assertType('object', $val); + }); + } + + /** + * @param array $objects + * + * @return array + */ + function baz(array $objects) : array + { + return array_filter($objects, static function ($val, $key) { + assertType('string', $key); + assertType('object', $val); + }, ARRAY_FILTER_USE_BOTH); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3133.php b/tests/PHPStan/Analyser/nsrt/bug-3133.php new file mode 100644 index 0000000000..c6516dfe70 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3133.php @@ -0,0 +1,58 @@ +|string', $arg); + return; + } + + assertType('numeric-string', $arg); + } + + /** + * @param string|bool|float|int|mixed[]|null $arg + */ + public function doBar($arg): void + { + if (\is_numeric($arg)) { + assertType('float|int|numeric-string', $arg); + } + } + + /** + * @param numeric $numeric + * @param numeric-string $numericString + */ + public function doBaz( + $numeric, + string $numericString + ) + { + assertType('float|int|numeric-string', $numeric); + assertType('numeric-string', $numericString); + } + + /** + * @param numeric-string $numericString + */ + public function doLorem( + string $numericString + ) + { + $a = []; + $a[$numericString] = 'foo'; + assertType('non-empty-array', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3134.php b/tests/PHPStan/Analyser/nsrt/bug-3134.php new file mode 100644 index 0000000000..43a47c2d7b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3134.php @@ -0,0 +1,24 @@ + + */ + private $instances = []; + + public function get(string $name) : object + { + return $this->instances[$name]; + } +} + +function (Registry $r): void { + assertType('bool', $r->get('x') === $r->get('x')); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3142.php b/tests/PHPStan/Analyser/nsrt/bug-3142.php new file mode 100644 index 0000000000..b1a53ee3cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3142.php @@ -0,0 +1,77 @@ +sayHi()); + assertType('string', $hw->sayHello()); +}; + +interface DecoratorInterface +{ +} + +class FooDecorator implements DecoratorInterface +{ + public function getCode(): string + { + return 'FOO'; + } +} + +trait DecoratorTrait +{ + public function getDecorator(): DecoratorInterface + { + return new FooDecorator(); + } +} + +/** + * @method FooDecorator getDecorator() + */ +class Dummy +{ + use DecoratorTrait; + + public function getLabel(): string + { + return $this->getDecorator()->getCode(); + } +} + +function () { + $dummy = new Dummy(); + assertType(FooDecorator::class, $dummy->getDecorator()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3158.php b/tests/PHPStan/Analyser/nsrt/bug-3158.php new file mode 100644 index 0000000000..e807a11299 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3158.php @@ -0,0 +1,67 @@ + $className + * @param \Closure(T):void $outmaker + * @return T + */ +function createProxy( + string $className, + \Closure $outmaker +) : object { + $t = new $className(); + $outmaker($t); + return $t; +} + +/** + * @template T as object + * @param \Closure(T, T):void $outmaker + * @return T + */ +function createProxy2( + \Closure $outmaker +) : object { + +} + +class AAParent {} + +class AParent extends AAParent {} + +class A extends AParent { + public function bar() : void {} +} + +class B extends A { + +} + +function (): void { + $proxy = createProxy(A::class, function(AParent $o):void {}); + assertType(A::class, $proxy); + + $proxy = createProxy(A::class, function($o):void {}); + assertType(A::class, $proxy); + + $proxy = createProxy(A::class, function(B $o):void {}); + assertType(A::class, $proxy); + + $proxy = createProxy2(function(A $a, B $o):void {}); + assertType(B::class, $proxy); +}; + +function (): void { + /** @var object $object */ + $object = doFoo(); + $objectClass = get_class($object); + assertType('class-string', $objectClass); + $proxy = createProxy($objectClass, function(AParent $o):void {}); + assertType('object', $proxy); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3190.php b/tests/PHPStan/Analyser/nsrt/bug-3190.php new file mode 100644 index 0000000000..7348ca3623 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3190.php @@ -0,0 +1,94 @@ +isDedicated() : false; + if (!$dedicated) { + return; + } + + assertType(Server::class, $component); + } + + public function deploy2(object $component): void + { + $dedicated = $component instanceof Server && $component->isDedicated(); + if (!$dedicated) { + return; + } + + assertType(Server::class, $component); + } + + public function deploy3(object $component): void + { + $dedicated = $component instanceof Server; + if (!$dedicated) { + return; + } + + assertType(Server::class, $component); + } + + public function deploy4(object $component): void + { + $dedicated = $component instanceof Server ? $component->isDedicated() : rand(0, 1); + if (!$dedicated) { + return; + } + + assertType('object', $component); + } +} + +class Deployer2 +{ + public function deploy(object $component): void + { + $dedicated = $component instanceof Server ? $component->isDedicated() : false; + if ($dedicated) { + assertType(Server::class, $component); + return; + } + } + + public function deploy2(object $component): void + { + $dedicated = $component instanceof Server && $component->isDedicated(); + if ($dedicated) { + assertType(Server::class, $component); + return; + } + } + + public function deploy3(object $component): void + { + $dedicated = $component instanceof Server; + if ($dedicated) { + assertType(Server::class, $component); + return; + } + } + + public function deploy4(object $component): void + { + $dedicated = $component instanceof Server ? $component->isDedicated() : rand(0, 1); + if ($dedicated) { + assertType('object', $component); + return; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3226.php b/tests/PHPStan/Analyser/nsrt/bug-3226.php similarity index 96% rename from tests/PHPStan/Analyser/data/bug-3226.php rename to tests/PHPStan/Analyser/nsrt/bug-3226.php index 9acec2e658..09b4668057 100644 --- a/tests/PHPStan/Analyser/data/bug-3226.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3226.php @@ -2,7 +2,7 @@ namespace Bug3226; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { diff --git a/tests/PHPStan/Analyser/nsrt/bug-3266.php b/tests/PHPStan/Analyser/nsrt/bug-3266.php new file mode 100644 index 0000000000..bee0c7e6f8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3266.php @@ -0,0 +1,32 @@ + $iterator + * @phpstan-return array + */ + public function iteratorToArray($iterator) + { + assertType('array', $iterator); + $array = []; + foreach ($iterator as $key => $value) { + assertType('TKey of (int|string) (method Bug3266\Foo::iteratorToArray(), argument)', $key); + assertType('TValue (method Bug3266\Foo::iteratorToArray(), argument)', $value); + $array[$key] = $value; + assertType('non-empty-array', $array); + } + + assertType('array', $array); + + return $array; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3269.php b/tests/PHPStan/Analyser/nsrt/bug-3269.php new file mode 100644 index 0000000000..4a0d7fef71 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3269.php @@ -0,0 +1,46 @@ +> $intervalGroups + */ + public static function bar(array $intervalGroups): void + { + $borders = []; + foreach ($intervalGroups as $group) { + foreach ($group as $interval) { + $borders[] = ['version' => $interval['start']->getVersion(), 'operator' => $interval['start']->getOperator(), 'side' =>'start']; + $borders[] = ['version' => $interval['end']->getVersion(), 'operator' => $interval['end']->getOperator(), 'side' =>'end']; + } + } + + assertType("list", $borders); + + foreach ($borders as $border) { + assertType("array{version: string, operator: string, side: 'end'|'start'}", $border); + assertType('\'end\'|\'start\'', $border['side']); + } + } + +} + +class Blah +{ + + public function getVersion(): string + { + return ''; + } + + public function getOperator(): string + { + return ''; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3276.php b/tests/PHPStan/Analyser/nsrt/bug-3276.php new file mode 100644 index 0000000000..53faad441b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3276.php @@ -0,0 +1,28 @@ + 'een', 'two' => 'twee', 'three' => 'drie']; + usort($arr, 'strcmp'); + assertType("non-empty-list<'drie'|'een'|'twee'>", $arr); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3321.php b/tests/PHPStan/Analyser/nsrt/bug-3321.php new file mode 100644 index 0000000000..a89c14b478 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3321.php @@ -0,0 +1,77 @@ +> $containers + * @return T + */ + function unwrap(array $containers) { + return array_map( + function ($container) { + return $container->get(); + }, + $containers + )[0]; + } + + /** + * @param array> $typed_containers + */ + function takesDifferentTypes(array $typed_containers): void { + assertType('int', $this->unwrap($typed_containers)); + } + +} + +/** + * @template TFacade of Facade + */ +interface Facadable +{ +} + +/** + * @implements Facadable + */ +class A implements Facadable {} + +/** + * @implements Facadable + */ +class B implements Facadable {} + +abstract class Facade {} +class AFacade extends Facade {} +class BFacade extends Facade {} + +class FacadeFactory { + /** + * @template TFacade of Facade + * @param class-string> $class + * @return TFacade + */ + public function create(string $class): Facade + { + // Returns facade for class + } +} + + +function (FacadeFactory $f): void { + $facadeA = $f->create(A::class); + assertType(AFacade::class, $facadeA); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3331.php b/tests/PHPStan/Analyser/nsrt/bug-3331.php new file mode 100644 index 0000000000..d1823d2936 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3331.php @@ -0,0 +1,23 @@ +', mb_convert_encoding($arr)); + \PHPStan\Testing\assertType('string', mb_convert_encoding($str)); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding($mixed)); + \PHPStan\Testing\assertType('array|string|false', mb_convert_encoding()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3351.php b/tests/PHPStan/Analyser/nsrt/bug-3351.php new file mode 100644 index 0000000000..468597455b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3351.php @@ -0,0 +1,32 @@ +combine($a, $b); + assertType("array<'a'|'b'|'c', 1|2|3>|false", $c); + + assertType('array{a: 1, b: 2, c: 3}', array_combine($a, $b)); + } + + /** + * @template TKey + * @template TValue + * @param array $keys + * @param array $values + * + * @return array|false + */ + private function combine(array $keys, array $values) + { + return array_combine($keys, $values); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3379.php b/tests/PHPStan/Analyser/nsrt/bug-3379.php new file mode 100644 index 0000000000..4500775f5a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3379.php @@ -0,0 +1,17 @@ + $s + */ + public function takesStringArray(array $s) : void{} + + public function takesString(string $s) : void{} + + public function main2(string $input) : void{ + if(is_array($var = json_decode($input))){ + assertType('array', $var); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3548.php b/tests/PHPStan/Analyser/nsrt/bug-3548.php new file mode 100644 index 0000000000..43d9568dec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3548.php @@ -0,0 +1,20 @@ + 3){ + $idGroups[] = [1,2]; + $idGroups[] = [1,2]; + $idGroups[] = [1,2]; + } + + if(count($idGroups) > 0){ + assertType('array{array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); + } +}; + +function (): void { + $idGroups = [1]; + + if(time() > 3){ + $idGroups[] = [1,2]; + $idGroups[] = [1,2]; + $idGroups[] = [1,2]; + } + + if(count($idGroups) > 1){ + assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3617.php b/tests/PHPStan/Analyser/nsrt/bug-3617.php new file mode 100644 index 0000000000..2930c4faa8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3617.php @@ -0,0 +1,24 @@ +value = $value; + } + + public function getValue(): string + { + return $this->value; + } +} + +class HelloWorld +{ + /** + * @return (Field|null)[] + */ + public function getValue(): array + { + $first = null; + $second = null; + if (isset($_POST['first']) && is_string($_POST['first']) && !empty($_POST['first'])) { + $first = new Field($_POST['first']); + } + if (isset($_POST['second']) && is_string($_POST['second']) && !empty($_POST['second'])) { + $second = new Field($_POST['second']); + } + return [$first, $second]; + } + + public function sayHello(): void + { + [$first, $second] = $this->getValue(); + if (!$first && !$second) { + echo 'empty'; + } elseif (!$first) { + assertType(Field::class, $second); + } elseif (!$second) { + assertType(Field::class, $first); + echo $first->getValue(); + } else { + assertType(Field::class, $first); + assertType(Field::class, $second); + echo $first->getValue() . "\n" . $second->getValue(); + } + } + + public function sayGoodbye(): void + { + [$first, $second] = $this->getValue(); + if ($first || $second) { + assertType(Field::class, $first ?: $second); + assertType(Field::class, $first ?? $second); + } + } + + public function sayGoodbye2(): void + { + [$first, $second] = $this->getValue(); + if ($first || $second) { + assertType(Field::class, $first ? $first : $second); + assertType(Field::class, $first ?? $second); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3710.php b/tests/PHPStan/Analyser/nsrt/bug-3710.php new file mode 100644 index 0000000000..171d7b9065 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3710.php @@ -0,0 +1,36 @@ +createPersonButCouldFail(); + } finally { + assertType('Bug3710\Person|null', $person); + } + + assertType('Bug3710\Person', $person); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3760.php b/tests/PHPStan/Analyser/nsrt/bug-3760.php new file mode 100644 index 0000000000..dd81edde82 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3760.php @@ -0,0 +1,47 @@ +allowsCovariance = (bool)$allowsCovariance; + + assertType('bool', $allowsCovariance); + assertNativeType('mixed', $allowsCovariance); + assertType('bool', $allowsContravariance); + assertNativeType('mixed', $allowsContravariance); + $this->allowsContravariance = (bool)$allowsContravariance; + + assertType('bool', $allowsCovariance); + assertNativeType('mixed', $allowsCovariance); + assertType('bool', $allowsContravariance); + assertNativeType('mixed', $allowsContravariance); + } +} 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 @@ +status = $status; + $this->body = $body; + } + + public function status(): int { return $this->status; } + public function body(): string { return $this->body; } +} + +class HelloWorld +{ + public function sayHello(Response $response): string + { + $body = $response->body(); + $status = $response->status(); + + if ($status !== 200) { + throw new \LogicException('NOOOO' . $body); + } + + try { + $payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + unset($body); + } catch (\JsonException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $body); + } + + return $payload['thing'] ?? 'default'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3853.php b/tests/PHPStan/Analyser/nsrt/bug-3853.php new file mode 100644 index 0000000000..ad2bf827a5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3853.php @@ -0,0 +1,25 @@ +} $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/nsrt/bug-3858.php b/tests/PHPStan/Analyser/nsrt/bug-3858.php new file mode 100644 index 0000000000..341e64a9f6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3858.php @@ -0,0 +1,25 @@ + $set */ + $set = new Set(); + foreach ($this->a() as $item) { + $set->add(\get_class($item)); + } + foreach ($this->b() as $item) { + $set->add(\get_class($item)); + } + foreach ($this->c() as $item) { + $set->add($item); + } + assertType('Ds\Set', $set); + $set->sort(); + } + + /** + * @return Iterator + */ + abstract public function a(): Iterator; + + /** + * @return Iterator + */ + private function b(): Iterator + { + yield new DateTimeImmutable(); + } + + /** + * @return Iterator> + */ + private function c(): Iterator + { + yield DateTimeImmutable::class; + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-3875.php b/tests/PHPStan/Analyser/nsrt/bug-3875.php new file mode 100644 index 0000000000..64d7b6fecf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3875.php @@ -0,0 +1,19 @@ + 9223372036854775807 || $num < -9223372036854775808) { + assertType('mixed', $value); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-3915.php b/tests/PHPStan/Analyser/nsrt/bug-3915.php new file mode 100644 index 0000000000..39a0addeee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3915.php @@ -0,0 +1,24 @@ +', $lengths); + } + + public static function getInt(): int + { + return 5; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3922.php b/tests/PHPStan/Analyser/nsrt/bug-3922.php new file mode 100644 index 0000000000..b65312a447 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3922.php @@ -0,0 +1,64 @@ + + */ +interface QueryHandlerInterface +{ + /** + * @param TQuery $query + * + * @return TResult + */ + public function handle(QueryInterface $query); +} + +/** + * @template TResult + */ +interface QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class FooQuery implements QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class BarQuery implements QueryInterface +{ +} + +final class FooQueryResult +{ +} + +final class BarQueryResult +{ +} + +/** + * @template-implements QueryHandlerInterface + */ +final class FooQueryHandler implements QueryHandlerInterface +{ + public function handle(QueryInterface $query) + { + return new FooQueryResult(); + } +} + +function (FooQueryHandler $h): void { + assertType(FooQueryResult::class, $h->handle(new FooQuery())); + assertType(FooQueryResult::class, $h->handle(new BarQuery())); +}; 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 @@ +nullable(); + $isNotNull = $result !== null; + + if ($isNotNull) { + assertType('int', $result); + } + if ($result !== null) { + assertType('int', $result); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3990.php b/tests/PHPStan/Analyser/nsrt/bug-3990.php new file mode 100644 index 0000000000..102745dd68 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3990.php @@ -0,0 +1,21 @@ +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/nsrt/bug-3993.php b/tests/PHPStan/Analyser/nsrt/bug-3993.php new file mode 100644 index 0000000000..a1a24e380d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3993.php @@ -0,0 +1,24 @@ +', count($arguments)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3997.php b/tests/PHPStan/Analyser/nsrt/bug-3997.php new file mode 100644 index 0000000000..850f2b7112 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3997.php @@ -0,0 +1,15 @@ +', $c->count()); + assertType('int<0, max>', count($c)); +}; + +function (\ArrayIterator $i): void { + assertType('int<0, max>', $i->count()); + assertType('int<0, max>', count($i)); +}; 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/nsrt/bug-4177.php b/tests/PHPStan/Analyser/nsrt/bug-4177.php new file mode 100644 index 0000000000..6620561b23 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4177.php @@ -0,0 +1,33 @@ +unixtimestamp : null; + } + + public function getPeriodTo(): ?int + { + return rand(0,1) ? $this->unixtimestamp : null; + } +} + +function (Dto $request): void { + if ($request->getPeriodFrom() || $request->getPeriodTo()) { + if ($request->getPeriodFrom()) { + assertType('int|int<1, max>', $request->getPeriodFrom()); + } + + if ($request->getPeriodTo() !== null) { + assertType('int', $request->getPeriodTo()); + } + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4188.php b/tests/PHPStan/Analyser/nsrt/bug-4188.php new file mode 100644 index 0000000000..a34cca26a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4188.php @@ -0,0 +1,40 @@ + $data */ + public function set(array $data): void + { + $filtered = array_filter( + $data, + function ($param): bool { + return $param instanceof B; + }, + ); + assertType('array', $filtered); + + $this->onlyB($filtered); + } + + /** @param array $data */ + public function setShort(array $data): void + { + $filtered = array_filter( + $data, + fn($param): bool => $param instanceof B, + ); + assertType('array', $filtered); + + $this->onlyB($filtered); + } + + /** @param B[] $data */ + public function onlyB(array $data): void {} +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4190.php b/tests/PHPStan/Analyser/nsrt/bug-4190.php new file mode 100644 index 0000000000..449f43aaef --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4190.php @@ -0,0 +1,16 @@ +>', range(1, 10000)); + assertType('non-empty-list>', range(10000, 1)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4209-2.php b/tests/PHPStan/Analyser/nsrt/bug-4209-2.php new file mode 100644 index 0000000000..9c93ab8f44 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4209-2.php @@ -0,0 +1,69 @@ + + */ +class CustomerLink implements Link +{ + /** + * @var Customer + */ + public $item; + + /** + * @param Customer $item + */ + public function __construct($item) { + $this->item = $item; + } + + /** + * @return Customer + */ + public function getItem() + { + return $this->item; + } +} + +/** + * @return CustomerLink[] + */ +function get_links(): array { + return [new CustomerLink(new Customer())]; +} + +/** + * @template T + * @param Link[] $links + * @return T + */ +function process_customers(array $links) { + // no-op +} + +class Runner { + public function run(): void + { + assertType('Bug4209Two\Customer', process_customers(get_links())); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4209.php b/tests/PHPStan/Analyser/nsrt/bug-4209.php new file mode 100644 index 0000000000..007c033bde --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4209.php @@ -0,0 +1,50 @@ +item = $item; + } +} + +class Customer +{ + public function getName(): string { return 'customer'; } +} + +/** + * @return Link[] + */ +function get_links(): array { + return [new Link(new Customer())]; +} + +/** + * @template T + * @param Link[] $links + * @return T + */ +function process_customers(array $links) { + // no-op +} + +class Runner { + public function run(): void + { + assertType('Bug4209\Customer', process_customers(get_links())); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4213.php b/tests/PHPStan/Analyser/nsrt/bug-4213.php new file mode 100644 index 0000000000..a8f06cb1c5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4213.php @@ -0,0 +1,65 @@ += 8.1 + +namespace Bug4213; + +use function PHPStan\Testing\assertType; + +abstract class BaseEnum +{ + /** @var string */ + private $value; + + final private function __construct(string $value) + { + $this->value = $value; + } + /** + * @return static + */ + public static function get(string $value): self { + return new static($value); + } +} + +final class Enum extends BaseEnum +{ +} + +final class Entity { + public function setEnums(Enum ...$enums): void { + } + /** + * @param Enum[] $enums + */ + public function setEnumsWithoutSplat(array $enums): void { + } +} + +function (): void { + assertType('Bug4213\Enum', Enum::get('test')); + assertType('array{Bug4213\\Enum}', array_map([Enum::class, 'get'], ['test'])); +}; + + +class Foo +{ + /** + * @return static + */ + public static function create() : Foo + { + return new static(); + } +} + + +class Bar extends Foo +{ +} + +function (): void { + $cbFoo = [Foo::class, 'create']; + $cbBar = [Bar::class, 'create']; + assertType('Bug4213\Foo', $cbFoo()); + assertType('Bug4213\Bar', $cbBar()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4215.php b/tests/PHPStan/Analyser/nsrt/bug-4215.php new file mode 100644 index 0000000000..7fd5aaf719 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4215.php @@ -0,0 +1,23 @@ + $b + */ + function test(int $a = null, array $b = null): void + { + if ($a === null && $b === null) return; + + if ($b === null) { + assertType('int', $a); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4231.php b/tests/PHPStan/Analyser/nsrt/bug-4231.php new file mode 100644 index 0000000000..73db62990f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4231.php @@ -0,0 +1,27 @@ +getValue($this))) { + assertType('null', $property->getValue($this)); + $property->setValue($this, 'Some value which is not null'); + assertType('mixed', $property->getValue($this)); + } +}; + +function (): void { + $property = new \ReflectionProperty($this, 'data'); + + if (is_null($property->getValue($this))) { + assertType('null', $property->getValue($this)); + $property->setValue($this, 'Some value which is not null'); + + $value = $property->getValue($this); + assertType('mixed', $value); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4247.php b/tests/PHPStan/Analyser/nsrt/bug-4247.php new file mode 100644 index 0000000000..a96c7d71cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4247.php @@ -0,0 +1,30 @@ + $classname + * @return TInit + */ + public static function init($classname) { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4267.php b/tests/PHPStan/Analyser/nsrt/bug-4267.php new file mode 100644 index 0000000000..c93256827b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4267.php @@ -0,0 +1,54 @@ + + */ +class Model1 implements \IteratorAggregate +{ + + #[\ReturnTypeWillChange] + public function getIterator(): iterable + { + throw new \Exception('not implemented'); + } +} + +class HelloWorld1 extends Model1 +{ + /** @var int */ + public $x = 5; +} + +function (): void { + foreach (new HelloWorld1() as $h) { + assertType(HelloWorld1::class, $h); + } +}; + +class Model2 implements \IteratorAggregate +{ + /** + * @return iterable + */ + #[\ReturnTypeWillChange] + public function getIterator(): iterable + { + throw new \Exception('not implemented'); + } +} + +class HelloWorld2 extends Model2 +{ + /** @var int */ + public $x = 5; +} + +function (): void { + foreach (new HelloWorld2() as $h) { + assertType(HelloWorld2::class, $h); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4287.php b/tests/PHPStan/Analyser/nsrt/bug-4287.php new file mode 100644 index 0000000000..646a313d49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4287.php @@ -0,0 +1,35 @@ + $obj + */ +function(\ArrayObject $obj): void { + if (count($obj) === 0) { + return; + } + + assertType('int<1, max>', count($obj)); + + $obj->offsetUnset(0); + + assertType('int<0, max>', count($obj)); +}; + +/** + * @param \ArrayObject $obj + */ +function(\ArrayObject $obj): void { + if (count($obj) === 0) { + return; + } + + assertType('int<1, max>', count($obj)); + + unset($obj[0]); + + assertType('int<0, max>', count($obj)); +}; 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 @@ + assertType('string', $value ?? '-'); + fn (?string $value): void => assertType('string|null', $value); + + $f = fn (?string $value): string => $value ?? '-'; + + assertType('string', $f($v)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4343.php b/tests/PHPStan/Analyser/nsrt/bug-4343.php new file mode 100644 index 0000000000..cbc0271df3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4343.php @@ -0,0 +1,16 @@ + 0) { + $test = new \stdClass(); + } + + foreach ($a as $my) { + assertVariableCertainty(TrinaryLogic::createYes(), $test); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4351.php b/tests/PHPStan/Analyser/nsrt/bug-4351.php new file mode 100644 index 0000000000..4cfd2fbe25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4351.php @@ -0,0 +1,71 @@ +thing = null; + } +} + +class HelloWorld extends ParentC +{ + public function __construct(Thing $thing) + { + assertType('Bug4351\Thing|null', $this->thing); + $this->thing = $thing; + assertType('Bug4351\Thing', $this->thing); + + parent::__construct(); + assertType('Bug4351\Thing|null', $this->thing); + } + + public function doFoo(Thing $thing) + { + assertType('Bug4351\Thing|null', $this->thing); + $this->thing = $thing; + assertType('Bug4351\Thing', $this->thing); + + UnrelatedClass::doFoo(); + assertType('Bug4351\Thing', $this->thing); + } + + public function doBar(Thing $thing) + { + assertType('Bug4351\Thing|null', $this->thing); + $this->thing = $thing; + assertType('Bug4351\Thing', $this->thing); + + UnrelatedClass::doStaticFoo(); + assertType('Bug4351\Thing', $this->thing); + } +} + +class UnrelatedClass +{ + + public function doFoo(): void + { + + } + + public static function doStaticFoo(): void + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4357.php b/tests/PHPStan/Analyser/nsrt/bug-4357.php new file mode 100644 index 0000000000..d8e36019c8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4357.php @@ -0,0 +1,26 @@ + */ + private $arr = null; + + public function test(): void { + if ($this->arr === null) { + return; + } + + assertType('array', $this->arr); + + unset($this->arr['hello']); + + assertType('array', $this->arr); + + if (count($this->arr) === 0) { + $this->arr = null; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4371.php b/tests/PHPStan/Analyser/nsrt/bug-4371.php new file mode 100644 index 0000000000..cd7237b5f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4371.php @@ -0,0 +1,26 @@ +', array_keys($meters)); + assertType('non-empty-list', array_values($meters)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4415.php b/tests/PHPStan/Analyser/nsrt/bug-4415.php new file mode 100644 index 0000000000..8dce55c08e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4415.php @@ -0,0 +1,25 @@ + + */ +class Foo implements \IteratorAggregate +{ + + public function getIterator(): \Iterator + { + + } + +} + +function (Foo $foo): void { + foreach ($foo as $k => $v) { + assertType('mixed', $k); // should be int + assertType('mixed', $v); // should be string + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4423.php b/tests/PHPStan/Analyser/nsrt/bug-4423.php new file mode 100644 index 0000000000..d136dc4d6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4423.php @@ -0,0 +1,64 @@ + $bar + * @method Bar doBar() + */ +trait Foo { + + /** @var Bar */ + public $baz; + + /** @param K $k */ + public function doFoo($k) + { + assertType('T (class Bug4423\Child, argument)', $k); + //assertType('Bug4423\Bar', $this->bar); + assertType('Bug4423\Bar', $this->baz); + //assertType('Bug4423\Bar', $this->doBar()); + assertType('Bug4423\Bar', $this->doBaz()); + } + + /** @return Bar */ + public function doBaz() + { + + } + +} + +/** + * @template T + * @template K + */ +class Base { + +} + +/** + * @template T + * @extends Base + */ +class Child extends Base { + /** @phpstan-use Foo */ + use Foo; +} + +function (Child $child): void { + /** @var Child $child */ + assertType('Bug4423\Child', $child); + //assertType('Bug4423\Bar', $child->bar); + assertType('Bug4423\Bar', $child->baz); + //assertType('Bug4423\Bar', $child->doBar()); + assertType('Bug4423\Bar', $child->doBaz()); +}; 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/nsrt/bug-4436.php b/tests/PHPStan/Analyser/nsrt/bug-4436.php new file mode 100644 index 0000000000..e442f55ca3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4436.php @@ -0,0 +1,31 @@ + */ + private $storage; + + public function __construct() + { + $this->storage = new \SplObjectStorage(); + } + + public function add(Bar $bar, string $value): void + { + $this->storage[$bar] = $value; + } + + public function get(Bar $bar): string + { + assertType('string', $this->storage[$bar]); + return $this->storage[$bar]; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4498.php b/tests/PHPStan/Analyser/nsrt/bug-4498.php new file mode 100644 index 0000000000..ad07baa3db --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4498.php @@ -0,0 +1,50 @@ + $iterable + * + * @return iterable + * + * @template TKey + * @template TValue + */ + public function fcn(iterable $iterable): iterable + { + if ($iterable instanceof \Traversable) { + assertType('iterable&Traversable', $iterable); + return $iterable; + } + + assertType('array', $iterable); + + return $iterable; + } + + /** + * @param iterable $iterable + * + * @return iterable + * + * @template TKey + * @template TValue + */ + public function bar(iterable $iterable): iterable + { + if (is_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; + } + + assertType('Traversable', $iterable); + + return $iterable; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4499.php b/tests/PHPStan/Analyser/nsrt/bug-4499.php new file mode 100644 index 0000000000..fe65958259 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4499.php @@ -0,0 +1,19 @@ + $things */ + function thing(array $things) : void{ + switch(count($things)){ + case 1: + assertType('array{int}', $things); + assertType('int', array_shift($things)); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4500.php b/tests/PHPStan/Analyser/nsrt/bug-4500.php new file mode 100644 index 0000000000..2aff665833 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4500.php @@ -0,0 +1,139 @@ + $modelPropertyParameter */ + $modelPropertyParameter = doFoo(); + + /** @var int $parameterIndex */ + /** @var \stdClass $modelType */ + [$parameterIndex, $modelType] = $modelPropertyParameter; + + assertType('int', $parameterIndex); + assertType('stdClass', $modelType); + } + + public function doAmet(array $slots): void + { + /** @var \stdClass[] $itemSlots */ + /** @var \stdClass[] $slots */ + $itemSlots = []; + + assertType('array', $itemSlots); + assertType('array', $slots); + } + + public function listDestructuring(): void + { + /** @var int $test */ + [[$test]] = doFoo(); + assertType('int', $test); + } + + public function listDestructuring2(): void + { + /** @var int $test */ + [$test] = doFoo(); + assertType('int', $test); + } + + public function listDestructuringForeach(): void + { + /** @var int $value */ + foreach (doFoo() as [[$value]]) { + assertType('int', $value); + } + } + + public function listDestructuringForeach2(): void + { + /** @var int $value */ + foreach (doFoo() as [$value]) { + assertType('int', $value); + } + } + + public function doConseteur(): void + { + /** + * @var int $foo + * @var string $bar + */ + [$foo, $bar] = doFoo(); + + assertType('int', $foo); + assertType('string', $bar); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4504.php b/tests/PHPStan/Analyser/nsrt/bug-4504.php new file mode 100644 index 0000000000..ceab5de4e2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4504.php @@ -0,0 +1,21 @@ + $models */ + foreach ($models as $k => $v) { + assertType('Bug4504TypeInference\A', $v); + } + + assertType('Iterator', $models); + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-4538.php b/tests/PHPStan/Analyser/nsrt/bug-4538.php new file mode 100644 index 0000000000..20be659998 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4538.php @@ -0,0 +1,17 @@ +', getenv()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4545.php b/tests/PHPStan/Analyser/nsrt/bug-4545.php new file mode 100644 index 0000000000..e7f48619cd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4545.php @@ -0,0 +1,43 @@ + $firstMap + * @param Map $secondMap + * @param Closure(TValue1, TValue2): bool $comparator + * + * @return Set + */ + function compareMaps(Map $firstMap, Map $secondMap, Closure $comparator): Set + { + $firstMapKeys = $firstMap->keys(); + $secondMapKeys = $secondMap->keys(); + $keys = $firstMapKeys->xor($secondMapKeys); + $intersect = $firstMapKeys->intersect($secondMapKeys); + 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('1|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); + } + + return $keys; + } + +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-4557.php b/tests/PHPStan/Analyser/nsrt/bug-4557.php new file mode 100644 index 0000000000..5418cc932a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4557.php @@ -0,0 +1,66 @@ + $class + * @return T&MockObject + */ + public function createMock($class) + { + } + +} + +/** + * @template T of Lorem + */ +class Bar extends Foo +{ + + public function doBar(): void + { + $mock = $this->createMock(\stdClass::class); + assertType('Bug4557\\MockObject&stdClass', $mock); + } + + /** @return T */ + public function doBaz() + { + + } + +} + +class Baz +{ + + /** + * @param Bar $barLorem + * @param Bar $barIpsum + */ + public function doFoo(Bar $barLorem, Bar $barIpsum): void + { + assertType('Bug4557\\Lorem', $barLorem->doBaz()); + assertType('Bug4557\\Ipsum', $barIpsum->doBaz()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4558.php b/tests/PHPStan/Analyser/nsrt/bug-4558.php new file mode 100644 index 0000000000..a40e33581a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4558.php @@ -0,0 +1,50 @@ +suggestions) > 0) { + assertType('non-empty-array', $this->suggestions); + assertType('int<1, max>', count($this->suggestions)); + $try = array_shift($this->suggestions); + + assertType('array', $this->suggestions); + assertType('int<0, max>', count($this->suggestions)); + + if (rand(0, 1)) { + return $try; + } + + assertType('array', $this->suggestions); + assertType('int<0, max>', count($this->suggestions)); + + // we might be out of suggested days, so load some more + if (count($this->suggestions) === 0) { + assertType('array{}', $this->suggestions); + assertType('0', count($this->suggestions)); + $this->createSuggestions(); + } + + assertType('array', $this->suggestions); + assertType('int<0, max>', count($this->suggestions)); + } + + return null; + } + + private function createSuggestions(): void + { + $this->suggestions[] = new DateTime; + } +} 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/nsrt/bug-4573.php b/tests/PHPStan/Analyser/nsrt/bug-4573.php new file mode 100644 index 0000000000..92fb321de5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4573.php @@ -0,0 +1,40 @@ +isSubclassOf(Test::class)) { + $instance = $refClass->newInstance(); + assertType(Test::class, $instance); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4579.php b/tests/PHPStan/Analyser/nsrt/bug-4579.php new file mode 100644 index 0000000000..4b7c095153 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4579.php @@ -0,0 +1,22 @@ + $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/nsrt/bug-4588.php b/tests/PHPStan/Analyser/nsrt/bug-4588.php new file mode 100644 index 0000000000..ca46ae1d2a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4588.php @@ -0,0 +1,26 @@ +b = $b; + } + public function getB(): ?b { + + return $this->b; + } +} + +class b{ + public function callB():bool {return true;} +} + +function (c $c): void { + if ($c->getB()) { + assertType(b::class, $c->getB()); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4592.php b/tests/PHPStan/Analyser/nsrt/bug-4592.php new file mode 100644 index 0000000000..5d66bf93c5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4592.php @@ -0,0 +1,30 @@ + + */ + private $contacts1 = []; + + /** + * @var array{names: array, emails: array} + */ + private $contacts2 = ['names' => [], 'emails' => []]; + + public function sayHello1(string $id): void + { + $name = $this->contacts1[$id]['name'] ?? null; + assertType('string|null', $name); + } + + public function sayHello2(string $id): void + { + $name = $this->contacts2['names'][$id] ?? null; + assertType('string|null', $name); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4602.php b/tests/PHPStan/Analyser/nsrt/bug-4602.php new file mode 100644 index 0000000000..2e843364e5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4602.php @@ -0,0 +1,21 @@ +', $limit - 1); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4606.php b/tests/PHPStan/Analyser/nsrt/bug-4606.php new file mode 100644 index 0000000000..aa06628417 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4606.php @@ -0,0 +1,23 @@ + $assigned + */ + +assertType(Foo::class, $this); +assertType('list', $assigned); + + +/** + * @var array + * @phpstan-var array{\stdClass, int} + */ +$foo = doFoo(); + +assertType('array{stdClass, int}', $foo); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4642.php b/tests/PHPStan/Analyser/nsrt/bug-4642.php new file mode 100644 index 0000000000..7281959f1f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4642.php @@ -0,0 +1,28 @@ + + * @phpstan-param class-string $className + * @phpstan-return T + */ + function getRepository(string $className): IRepository; +} + +class User implements IEntity {} +/** @implements IRepository */ +class UsersRepository implements IRepository {} + +function (I $model): void { + assertType(UsersRepository::class, $model->getRepository(UsersRepository::class)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4650.php b/tests/PHPStan/Analyser/nsrt/bug-4650.php new file mode 100644 index 0000000000..f51b260c26 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4650.php @@ -0,0 +1,27 @@ + $idx + */ + function doFoo(array $idx): void { + assertType('non-empty-array', $idx); + assertNativeType('array', $idx); + + assertType('array{}', []); + assertNativeType('array{}', []); + + assertType('false', $idx === []); + assertNativeType('bool', $idx === []); + assertType('true', $idx !== []); + assertNativeType('bool', $idx !== []); + } + +} 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 @@ + + */ + function foo($u=null, $a=null){ + if (is_null($u) && is_null($a)){ + return [0,0]; + } + if ($u){ + $a = $u; + }else if ($a){ + $u = $a; + } + assertType('int|null', $u); + assertType('int|null', $a); + return [$u, $a]; + } + + /** + * @param int|null $u + * @param int|null $a + * @return array + */ + function bar($u=null, $a=null){ + if (!$u && !$a){ + return [0,0]; + } + if ($u){ + $a = $u; + }else if ($a){ + $u = $a; + } + assertType('int|int<1, max>', $u); + assertType('int|int<1, max>', $a); + return [$u, $a]; + } + + /** + * @param int|null $u + * @param int|null $a + * @return array + */ + function baz($u=null, $a=null){ + if (is_null($u) && is_null($a)){ + return [0,0]; + } + if ($u !== null){ + $a = $u; + }else if ($a !== null){ + $u = $a; + } + assertType('int', $u); + assertType('int', $a); + return [$u, $a]; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4700.php b/tests/PHPStan/Analyser/nsrt/bug-4700.php new file mode 100644 index 0000000000..9d386b0c50 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4700.php @@ -0,0 +1,49 @@ +', $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/nsrt/bug-4707.php b/tests/PHPStan/Analyser/nsrt/bug-4707.php new file mode 100644 index 0000000000..309becc933 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4707.php @@ -0,0 +1,55 @@ + 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/nsrt/bug-4714.php b/tests/PHPStan/Analyser/nsrt/bug-4714.php new file mode 100644 index 0000000000..3185b4a0d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4714.php @@ -0,0 +1,24 @@ +prepare('SELECT `col` FROM `foo` WHERE `param` = :param'); + $statement->bindParam(':param', $param); + + assertVariableCertainty(TrinaryLogic::createYes(), $param); + + $param = 1; + + $statement->execute(); + $statement->bindColumn('col', $col); + + assertVariableCertainty(TrinaryLogic::createYes(), $col); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4725.php b/tests/PHPStan/Analyser/nsrt/bug-4725.php new file mode 100644 index 0000000000..e69631b578 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4725.php @@ -0,0 +1,50 @@ + $mapper + */ + public function __construct(Clx_Model_Mapper_Abstract $mapper) + { + } + + /** @return T */ + public function getT() + { + + } +} + +/** + * @template T of Application_Model_Ada + * @extends Clx_Model_Mapper_Abstract + */ +class ClxProductNet_Model_Mapper_Ada extends Clx_Model_Mapper_Abstract +{ + + public function x() { + $map = new Clx_Paginator_Adapter_Mapper($this); + assertType('Bug4725\Clx_Paginator_Adapter_Mapper', $map); + assertType('T of Bug4725\Application_Model_Ada (class Bug4725\ClxProductNet_Model_Mapper_Ada, argument)', $map->getT()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4733.php b/tests/PHPStan/Analyser/nsrt/bug-4733.php new file mode 100644 index 0000000000..dec6f9bd3b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4733.php @@ -0,0 +1,85 @@ + + */ + private $nodes; + + /** + * @phpstan-param array $nodes + */ + public function __construct(array $nodes) + { + $this->nodes = $nodes; + } + + public function splice(int $offset, int $length): void + { + $newNodes = array_splice($this->nodes, $offset, $length); + + assertType('array', $this->nodes); + assertType('array', $newNodes); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-4757.php b/tests/PHPStan/Analyser/nsrt/bug-4757.php new file mode 100644 index 0000000000..9b620ea29e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4757.php @@ -0,0 +1,369 @@ += 8.0 + +namespace Bug4757; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(?Reservation $oldReservation): void + { + if ($oldReservation?->isFoo()) { + assertType(Reservation::class, $oldReservation); + assertType('true', $oldReservation->isFoo()); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + } + + public function sayHello2(?Reservation $oldReservation): void + { + if (!$oldReservation?->isFoo()) { + assertType(Reservation::class . '|null', $oldReservation); + assertType('bool', $oldReservation->isFoo()); + return; + } + + assertType(Reservation::class, $oldReservation); + assertType('true', $oldReservation->isFoo()); + } + + public function sayHello3(?Reservation $oldReservation): void + { + if ($oldReservation?->isFoo() === true) { + assertType(Reservation::class, $oldReservation); + assertType('true', $oldReservation->isFoo()); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + assertType('bool', $oldReservation->isFoo()); + } + + public function sayHello4(?Reservation $oldReservation): void + { + if ($oldReservation?->isFoo() === false) { + assertType(Reservation::class , $oldReservation); + assertType('false', $oldReservation->isFoo()); + return; + } + + //assertType(Reservation::class . '|null', $oldReservation); + assertType('bool', $oldReservation->isFoo()); + } + + public function sayHello5(?Reservation $oldReservation): void + { + if ($oldReservation?->isFoo() === null) { + assertType(Reservation::class . '|null', $oldReservation); + return; + } + + assertType(Reservation::class, $oldReservation); + } + + public function sayHello6(?Reservation $oldReservation): void + { + if ($oldReservation?->isFoo() !== null) { + assertType(Reservation::class, $oldReservation); + assertType('bool', $oldReservation->isFoo()); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + assertType('bool', $oldReservation->isFoo()); + } + + public function sayHelloPure(?Reservation $oldReservation): void + { + if ($oldReservation?->isFoo()) { + assertType(Reservation::class, $oldReservation); + assertType('true', $oldReservation->isFoo()); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + } + + public function sayHelloImpure(?Reservation $oldReservation): void + { + if ($oldReservation?->isFooImpure()) { + assertType(Reservation::class, $oldReservation); + assertType('bool', $oldReservation->isFooImpure()); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + } + + public function sayHello2Impure(?Reservation $oldReservation): void + { + if (!$oldReservation?->isFooImpure()) { + assertType(Reservation::class . '|null', $oldReservation); + return; + } + + assertType(Reservation::class, $oldReservation); + } + + public function sayHello3Impure(?Reservation $oldReservation): void + { + if ($oldReservation?->isFooImpure() === true) { + assertType(Reservation::class, $oldReservation); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + } + + public function sayHello4Impure(?Reservation $oldReservation): void + { + if ($oldReservation?->isFooImpure() === false) { + assertType(Reservation::class , $oldReservation); + return; + } + + //assertType(Reservation::class . '|null', $oldReservation); + } + + public function sayHello5Impure(?Reservation $oldReservation): void + { + if ($oldReservation?->isFooImpure() === null) { + assertType(Reservation::class . '|null', $oldReservation); + return; + } + + assertType(Reservation::class, $oldReservation); + } + + public function sayHello6Impure(?Reservation $oldReservation): void + { + if ($oldReservation?->isFooImpure() !== null) { + assertType(Reservation::class, $oldReservation); + return; + } + + assertType(Reservation::class . '|null', $oldReservation); + } +} + +interface Reservation { + public function isFoo(): bool; + + /** @phpstan-impure */ + public function isFooImpure(): bool; +} + +interface Bar +{ + public function get(): ?int; + + /** @phpstan-impure */ + public function getImpure(): ?int; +} + +class Foo +{ + + public function getBarOrNull(): ?Bar + { + return null; + } + + public function doFoo(Bar $b): void + { + $barOrNull = $this->getBarOrNull(); + if ($barOrNull?->get() === null) { + assertType(Bar::class . '|null', $barOrNull); + assertType('int|null', $barOrNull->get()); + //assertType('null', $barOrNull?->get()); + return; + } + + assertType(Bar::class, $barOrNull); + assertType('int', $barOrNull->get()); + assertType('int', $barOrNull?->get()); + } + + public function doFooImpire(Bar $b): void + { + $barOrNull = $this->getBarOrNull(); + if ($barOrNull?->getImpure() === null) { + assertType(Bar::class . '|null', $barOrNull); + assertType('int|null', $barOrNull->getImpure()); + assertType('int|null', $barOrNull?->getImpure()); + return; + } + + assertType(Bar::class, $barOrNull); + assertType('int|null', $barOrNull->getImpure()); + assertType('int|null', $barOrNull?->getImpure()); + } + + public function doFoo2(Bar $b): void + { + $barOrNull = $this->getBarOrNull(); + if ($barOrNull?->get() !== null) { + assertType(Bar::class, $barOrNull); + assertType('int', $barOrNull->get()); + assertType('int', $barOrNull?->get()); + return; + } + + assertType(Bar::class . '|null', $barOrNull); + assertType('int|null', $barOrNull->get()); + } + + public function doFoo2Impure(Bar $b): void + { + $barOrNull = $this->getBarOrNull(); + if ($barOrNull?->getImpure() !== null) { + assertType(Bar::class, $barOrNull); + assertType('int|null', $barOrNull->getImpure()); + assertType('int|null', $barOrNull?->getImpure()); + return; + } + + assertType(Bar::class . '|null', $barOrNull); + assertType('int|null', $barOrNull->getImpure()); + assertType('int|null', $barOrNull?->getImpure()); + } + + public function doFoo3(Bar $b): void + { + $barOrNull = $this->getBarOrNull(); + if ($barOrNull?->get()) { + assertType(Bar::class, $barOrNull); + assertType('int|int<1, max>', $barOrNull->get()); + return; + } + + assertType(Bar::class . '|null', $barOrNull); + assertType('int|null', $barOrNull->get()); + } + + public function doFoo3Impure(Bar $b): void + { + $barOrNull = $this->getBarOrNull(); + if ($barOrNull?->getImpure()) { + assertType(Bar::class, $barOrNull); + assertType('int|null', $barOrNull->getImpure()); + return; + } + + assertType(Bar::class . '|null', $barOrNull); + assertType('int|null', $barOrNull->getImpure()); + } + +} + +class Chain +{ + + /** @var int */ + private $baz; + + /** @var self|null */ + private $selfOrNull; + + /** @var self */ + private $self; + + public function find(): ?self + { + + } + + public function get(): self + { + + } + + /** @phpstan-impure */ + public function findImpure(): ?self + { + + } + + public function doFoo(): void + { + assertType('int', $this->baz); + assertType('int|null', $this->find()?->baz); + assertType('int|null', $this->findImpure()?->baz); + } + + public function doBar(): void + { + if ($this->selfOrNull?->find()?->baz !== null) { + assertType(self::class, $this->selfOrNull); + assertType(self::class, $this->selfOrNull->find()); + } + } + + public function doBar2(): void + { + if ($this->selfOrNull?->find()?->get()->baz !== null) { + assertType(self::class, $this->selfOrNull); + assertType(self::class, $this->selfOrNull->find()); + } + } + + public function doBar3(): void + { + if ($this->selfOrNull?->find()?->get()->find() !== null) { + assertType(self::class, $this->selfOrNull); + assertType(self::class, $this->selfOrNull->find()); + } + } + + public function doBaz(): void + { + if ($this->selfOrNull?->findImpure()->baz !== null) { + assertType(self::class, $this->selfOrNull); + assertType(self::class . '|null', $this->selfOrNull->findImpure()); + } + } + + public function doBaz2(): void + { + if ($this->selfOrNull?->self->baz !== null) { + assertType(self::class, $this->selfOrNull); + } + } + + public function doBaz3(): void + { + if ($this->selfOrNull?->findImpure()->baz === 1) { + assertType(self::class, $this->selfOrNull); + assertType(self::class . '|null', $this->selfOrNull->findImpure()); + } + } + + public function doBaz4(): void + { + if ($this->selfOrNull?->find()?->get()->baz === 1) { + assertType(self::class, $this->selfOrNull); + assertType(self::class, $this->selfOrNull->find()); + } + } + + public function doVariable(): void + { + $foo = $this->selfOrNull; + if ($foo?->get()->selfOrNull !== null) { + assertType(self::class, $foo); + assertType(self::class, $foo->get()->selfOrNull); + } + } + + public function doLorem(): void + { + if ($this->find()?->find()?->find() !== null) { + assertType(self::class, $this->find()); + assertType(self::class, $this->find()->find()); + assertType(self::class, $this->find()->find()->find()); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4761.php b/tests/PHPStan/Analyser/nsrt/bug-4761.php new file mode 100644 index 0000000000..d9223dde62 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4761.php @@ -0,0 +1,17 @@ +|T $proxyOrObject + * @return T + */ + public function doFoo($proxyOrObject) + { + assertType('Bug4803\Proxy|T of object (method Bug4803\Foo::doFoo(), argument)', $proxyOrObject); + } + + /** @param Proxy<\stdClass> $proxy */ + public function doBar($proxy): void + { + assertType('stdClass', $this->doFoo($proxy)); + } + + /** @param \stdClass $std */ + public function doBaz($std): void + { + assertType('stdClass', $this->doFoo($std)); + } + + /** @param Proxy<\stdClass>|\stdClass $proxyOrStd */ + public function doLorem($proxyOrStd): void + { + assertType('stdClass', $this->doFoo($proxyOrStd)); + } + +} + +interface ProxyClassResolver +{ + /** + * @template T of object + * @param class-string>|class-string $className + * @return class-string + */ + public function resolveClassName(string $className): string; +} + +final class Client +{ + /** @var ProxyClassResolver */ + private $proxyClassResolver; + + public function __construct(ProxyClassResolver $proxyClassResolver) + { + $this->proxyClassResolver = $proxyClassResolver; + } + + /** + * @template T of object + * @param class-string>|class-string $className + * @return class-string + */ + public function getRealClass(string $className): string + { + assertType('class-string>|class-string', $className); + + $result = $this->proxyClassResolver->resolveClassName($className); + assertType('class-string', $result); + + return $result; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4814.php b/tests/PHPStan/Analyser/nsrt/bug-4814.php new file mode 100644 index 0000000000..2fcaf231f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4814.php @@ -0,0 +1,40 @@ +sendRequest($request); + $body = (string) $response->getBody(); + $decodedResponseBody = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $exception) { + assertType('string|null', $body); + assertType('array{}', $decodedResponseBody); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4816.php b/tests/PHPStan/Analyser/nsrt/bug-4816.php new file mode 100644 index 0000000000..12935f7e44 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4816.php @@ -0,0 +1,47 @@ +foo) { + $this->mayThrow(); + } + + } catch (\Throwable $e) { + assertType('bool', $param->foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + throw $e; + } + } + + /** + * @throws \RuntimeException + */ + private function mayThrow(): void + { + throw new \RuntimeException(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4821.php b/tests/PHPStan/Analyser/nsrt/bug-4821.php new file mode 100644 index 0000000000..9e9fbfa774 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4821.php @@ -0,0 +1,22 @@ +invoke($object); + return; + } catch (\ReflectionException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $object); + assertVariableCertainty(TrinaryLogic::createMaybe(), $method); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4822.php b/tests/PHPStan/Analyser/nsrt/bug-4822.php new file mode 100644 index 0000000000..2f730eaa45 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4822.php @@ -0,0 +1,33 @@ +test(); + + if (is_array($response)) { + $this->save(); + } + } catch (\Exception $e) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $response); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4838.php b/tests/PHPStan/Analyser/nsrt/bug-4838.php new file mode 100644 index 0000000000..19f8022517 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4838.php @@ -0,0 +1,24 @@ + 5) { + throw new \LogicException(); + } + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $handle); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4843.php b/tests/PHPStan/Analyser/nsrt/bug-4843.php new file mode 100644 index 0000000000..ab021b55af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4843.php @@ -0,0 +1,17 @@ +', $this->depth + ($isRoot ? 0 : 1)); + } +} 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/nsrt/bug-4879.php b/tests/PHPStan/Analyser/nsrt/bug-4879.php new file mode 100644 index 0000000000..26d74b1c73 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4879.php @@ -0,0 +1,44 @@ +test(); + } catch (\Throwable $ex) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $var); + } + } + + public function sayHello2(bool $bool1): void + { + try { + if ($bool1) { + throw new \Exception(); + } + + $var = 'foo'; + + $this->test(); + } catch (\Exception $ex) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $var); + } + } + + public function test(): void + { + return; + } +} 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/nsrt/bug-4887.php b/tests/PHPStan/Analyser/nsrt/bug-4887.php new file mode 100644 index 0000000000..ab0a549013 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4887.php @@ -0,0 +1,18 @@ + $_REQUEST, + '$_COOKIE' => $_COOKIE, + '$_SERVER' => $_SERVER, + '$GLOBALS' => $GLOBALS, + '$SESSION' => isset($_SESSION) ? $_SESSION : NULL]; + +foreach ($foo as $data) +{ + assertType('bool', is_array($data)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4896.php b/tests/PHPStan/Analyser/nsrt/bug-4896.php new file mode 100644 index 0000000000..c8345a364e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4896.php @@ -0,0 +1,38 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug4896; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(\DateTime|\DateInterval $command): void + { + switch ($command::class) { + case \DateTime::class: + assertType(\DateTime::class, $command); + break; + case \DateInterval::class: + assertType(\DateInterval::class, $command); + break; + } + + } + +} + +class Bar +{ + + public function doFoo(\DateTime|\DateInterval $command): void + { + match ($command::class) { + \DateTime::class => assertType(\DateTime::class, $command), + \DateInterval::class => assertType(\DateInterval::class, $command), + }; + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-4903.php b/tests/PHPStan/Analyser/nsrt/bug-4903.php new file mode 100644 index 0000000000..4b6b1fd459 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4903.php @@ -0,0 +1,27 @@ + $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 @@ +importFile)) { + return 1; + } + assertType('true', \file_exists($this->importFile)); + $this->importFile = '/b'; + assertType('bool', \file_exists($this->importFile)); + + if (\file_exists($this->importFile)) { + echo 'test'; + } + + return \file_exists($this->importFile) ? 0 : 1; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4982.php b/tests/PHPStan/Analyser/nsrt/bug-4982.php new file mode 100644 index 0000000000..3da5511850 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4982.php @@ -0,0 +1,22 @@ +doBar())) { + assertType('int', $c); + } + } + + public function doBar(): ?int + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4985.php b/tests/PHPStan/Analyser/nsrt/bug-4985.php new file mode 100644 index 0000000000..0bf67c11ce --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4985.php @@ -0,0 +1,37 @@ +setApp(new class() { + /** @var string */ + public $name = 'app'; + /** @var int */ + public $max_name_length = 40; + }); + + /** @var FieldMockCustom $surnameField */ + $surnameField = $m->addField('surname', [FieldMockCustom::class]); + + assertType(FieldMockCustom::class, $surnameField); + } + + public function doBar() + { + $m = new CollectionMockWithApp(); + $m->setApp(); + + /** @var FieldMockCustom $surnameField */ + $surnameField = $m->addField('surname', [FieldMockCustom::class]); + + assertType(FieldMockCustom::class, $surnameField); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5000.php b/tests/PHPStan/Analyser/nsrt/bug-5000.php new file mode 100644 index 0000000000..da5edf9613 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5000.php @@ -0,0 +1,53 @@ + $f + */ + function foo(F $f): void { + $values = $f->getValues(); + assertType('array', $values); + + $f->getDetail($values[0]); + assertType('array', $values); + foreach($values as $val) { + assertType('T of Bug5000\A|Bug5000\B (method Bug5000\Foo::foo(), argument)', $val); + $f->getDetail($val); + } + } + + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5017.php b/tests/PHPStan/Analyser/nsrt/bug-5017.php new file mode 100644 index 0000000000..f90994ee89 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5017.php @@ -0,0 +1,56 @@ +', $items); + $batch = array_splice($items, 0, 2); + assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('list<0|1|2|3|4>', $batch); + } + } + + /** + * @param int[] $items + */ + public function doBar($items) + { + while ($items) { + assertType('non-empty-array', $items); + $batch = array_splice($items, 0, 2); + assertType('array', $items); + assertType('array', $batch); + } + } + + public function doBar2() + { + $items = [0, 1, 2, 3, 4]; + assertType('array{0, 1, 2, 3, 4}', $items); + $batch = array_splice($items, 0, 2); + assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('array{0, 1}', $batch); + } + + /** + * @param int[] $ints + * @param string[] $strings + */ + public function doBar3(array $ints, array $strings) + { + $removed = array_splice($ints, 0, 2, $strings); + assertType('array', $removed); + assertType('array', $ints); + assertType('array', $strings); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-505.php b/tests/PHPStan/Analyser/nsrt/bug-505.php new file mode 100644 index 0000000000..ad21de9364 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-505.php @@ -0,0 +1,29 @@ +resolve($x); + } catch (\Throwable $e) { + assertType('int', $x); + } + + return 'bar'; + } + + private function resolve(int $x) : string + { + return 'ok'; + } + + private function handleError(int $x) : string + { + return 'error'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5072.php b/tests/PHPStan/Analyser/nsrt/bug-5072.php new file mode 100644 index 0000000000..25f273346b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5072.php @@ -0,0 +1,29 @@ + $params + */ + public function incorrect(array $params): void + { + $page = isset($params['page']) ? intval($params['page']) : 1; + assertType('int<1, max>', max(1, $page)); + } + + public function incorrectWithConstant(): void + { + assertType('2147483647|9223372036854775807', max(1, PHP_INT_MAX)); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-5129.php b/tests/PHPStan/Analyser/nsrt/bug-5129.php new file mode 100644 index 0000000000..925594b386 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5129.php @@ -0,0 +1,63 @@ +foo = ''; + assertType('0', strlen($this->foo)); + if (strlen($this->foo) > 0) { + return; + } + + assertType('0', strlen($this->foo)); + + $this->foo = 'x'; + assertType('1', strlen($this->foo)); + if (strlen($this->foo) > 0) { + return; + } + + assertType('0', strlen($this->foo)); + + $this->foo = $s; + assertType('int<0, max>', strlen($this->foo)); + } + + public function sayHello2(string $s): void + { + $this->foo = ''; + if (!$this->isFoo($this->foo)) { + return; + } + + assertType('true', $this->isFoo($this->foo)); + + $this->foo = 'x'; + assertType('bool', $this->isFoo($this->foo)); + if (!$this->isFoo($this->foo)) { + return; + } + assertType('true', $this->isFoo($this->foo)); + + $this->foo = $s; + assertType('bool', $this->isFoo($this->foo)); + if (!$this->isFoo($this->foo)) { + return; + } + assertType('true', $this->isFoo($this->foo)); + } + + public function isFoo(string $s): bool + { + return strlen($s) % 3; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5140.php b/tests/PHPStan/Analyser/nsrt/bug-5140.php new file mode 100644 index 0000000000..aaeeb99d6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5140.php @@ -0,0 +1,43 @@ + + */ + public function toArray(): array; + + /** + * @param TKey $key + * @param T $value + */ + public function set($key, $value): void; +} + +class Foo +{ + + /** + * @template TKey of array-key + * @template T of object + * @param Collection $in + * @param Collection $out + */ + function test(Collection $in, Collection $out): void + { + foreach($in->toArray() as $key => $value) { + assertType('TKey of (int|string) (method Bug5140\Foo::test(), argument)', $key); + assertType('T of object (method Bug5140\Foo::test(), argument)', $value); + $out->set($key, $value); + } + } + +} 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/nsrt/bug-5219.php b/tests/PHPStan/Analyser/nsrt/bug-5219.php new file mode 100644 index 0000000000..39d1540111 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5219.php @@ -0,0 +1,25 @@ +', [$header => $message]); + } + + protected function bar(string $message): void + { + $header = sprintf('%s-%s', '', ''); + + assertType('\'-\'', $header); + assertType('array{-: string}', [$header => $message]); + } +} 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/nsrt/bug-5259.php b/tests/PHPStan/Analyser/nsrt/bug-5259.php new file mode 100644 index 0000000000..35e9bca126 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5259.php @@ -0,0 +1,30 @@ + $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/nsrt/bug-5293.php b/tests/PHPStan/Analyser/nsrt/bug-5293.php new file mode 100644 index 0000000000..40d8ca8cc4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5293.php @@ -0,0 +1,58 @@ + 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/nsrt/bug-5316.php b/tests/PHPStan/Analyser/nsrt/bug-5316.php new file mode 100644 index 0000000000..13dbf2a179 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5316.php @@ -0,0 +1,25 @@ + 'foo', + 2 => 'foo', + 3 => 'bar', + ]; + $names = ['foo', 'bar', 'baz']; + $array = ['foo' => [], 'bar' => [], 'baz' => []]; + + foreach ($map as $value => $name) { + $array[$name][] = $value; + } + + + foreach ($array as $name => $elements) { + assertType('bool', count($elements) > 0); + assertType('list<1|2|3>', $elements); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5322.php b/tests/PHPStan/Analyser/nsrt/bug-5322.php new file mode 100644 index 0000000000..71965701da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5322.php @@ -0,0 +1,35 @@ +produceIntOrNull(); + } + + assertType('int', $int); + } + + function doBar() + { + $int = $this->produceIntOrNull(); + while (!is_int($int)) { + $int = $this->produceIntOrNull(); + } + + assertType('int', $int); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5328.php b/tests/PHPStan/Analyser/nsrt/bug-5328.php new file mode 100644 index 0000000000..5bb7c7d301 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5328.php @@ -0,0 +1,25 @@ +produceIntOrNull(); + for ($i = 0; $i < 5 && !is_int($int) ; $i++) { + $int = $this->produceIntOrNull(); + } + + assertType('int|null', $int); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-5458.php b/tests/PHPStan/Analyser/nsrt/bug-5458.php new file mode 100644 index 0000000000..4dd1da8451 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5458.php @@ -0,0 +1,23 @@ +prop2 = 5; + + if ($this->isBroken()){ + return; + } + + assertType('false', $this->isBroken()); + assertType('5', $this->prop2); + + $this->damage = min($this->damage + $amount, 5); + + assertType('bool', $this->isBroken()); + assertType('5', $this->prop2); + } + + public function applyDamage2(int $amount): void + { + $this->prop2 = 5; + + if ($this->isBroken()){ + return; + } + + assertType('false', $this->isBroken()); + assertType('5', $this->prop2); + + $this->array['foo'] = min($this->damage + $amount, 5); + + assertType('bool', $this->isBroken()); + assertType('5', $this->prop2); + } + + protected function onBroken(): void + { + + } + + public function isBroken(): bool{ + return $this->damage >= 5; + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-5529.php b/tests/PHPStan/Analyser/nsrt/bug-5529.php new file mode 100644 index 0000000000..1e787f602e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5529.php @@ -0,0 +1,24 @@ +run()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5530.php b/tests/PHPStan/Analyser/nsrt/bug-5530.php new file mode 100644 index 0000000000..6cf8dd8f9a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5530.php @@ -0,0 +1,29 @@ +', $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/nsrt/bug-5584.php b/tests/PHPStan/Analyser/nsrt/bug-5584.php new file mode 100644 index 0000000000..45e6efeaa3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5584.php @@ -0,0 +1,24 @@ + 5]; + } + + $b = []; + + if (rand(0,1) === 0) { + $b = ['b' => 6]; + } + + assertType('array{}|array{b?: 6, a?: 5}', $a + $b); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-560.php b/tests/PHPStan/Analyser/nsrt/bug-560.php new file mode 100644 index 0000000000..93733a7e9e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-560.php @@ -0,0 +1,22 @@ +getRandomSuit()); + } +} 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/nsrt/bug-5675.php b/tests/PHPStan/Analyser/nsrt/bug-5675.php new file mode 100644 index 0000000000..bde301d4ec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5675.php @@ -0,0 +1,55 @@ +): void)|array|Bar $column + */ + public function foo($column): void + { + // ... + } + + public function bar() + { + /** @var Hello */ + $a = new Hello; + + $a->foo(function (Hello $h) : void { + assertType('Bug5675\Hello', $h); + }); + } +} + +/** + * @template T + */ +class Hello2 +{ + /** + * @param (\Closure(static): void)|array|Bar $column + */ + public function foo($column): void + { + // ... + } + + public function bar() + { + /** @var Hello2 */ + $a = new Hello2; + + $a->foo(function (Hello2 $h) : void { + \PHPStan\Testing\assertType('Bug5675\Hello2', $h); + }); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-5698-php8.php b/tests/PHPStan/Analyser/nsrt/bug-5698-php8.php new file mode 100644 index 0000000000..fb54d36cff --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5698-php8.php @@ -0,0 +1,16 @@ += 8.0 + +namespace Bug5698; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class FooPHP8 +{ + + function foo(int ...$foo): void { + assertType('array', $foo); + assertNativeType('array', $foo); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5759.php b/tests/PHPStan/Analyser/nsrt/bug-5759.php new file mode 100644 index 0000000000..e3511e869b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5759.php @@ -0,0 +1,39 @@ + $fields */ + function strict(array $fields): void + { + assertType('bool', in_array(ITF::FIELD_A, $fields, true)); + } + + + /** @param array $fields */ + function loose(array $fields): void + { + assertType('bool', in_array(ITF::FIELD_A, $fields, false)); + } + + function another(): void + { + /** @var array<'source'|'dist'> $arr */ + $arr = ['source']; + + assertType('bool', in_array('dist', $arr, true)); + assertType('bool', in_array('dist', $arr)); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-5817.php b/tests/PHPStan/Analyser/nsrt/bug-5817.php new file mode 100644 index 0000000000..ff7a7ca765 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5817.php @@ -0,0 +1,127 @@ + + * @implements Iterator + */ +class MyContainer implements + ArrayAccess, + Countable, + Iterator, + JsonSerializable +{ + /** @var array */ + protected $items = []; + + public function add(DateTimeInterface $item, int $offset = null): self + { + $this->offsetSet($offset, $item); + return $this; + } + + public function count(): int + { + return count($this->items); + } + + /** @return DateTimeInterface|false */ + #[\ReturnTypeWillChange] + public function current() + { + return current($this->items); + } + + /** @return DateTimeInterface|false */ + #[\ReturnTypeWillChange] + public function next() + { + return next($this->items); + } + + /** @return int|null */ + public function key(): ?int + { + return key($this->items); + } + + public function valid(): bool + { + return $this->key() !== null; + } + + /** @return DateTimeInterface|false */ + #[\ReturnTypeWillChange] + public function rewind() + { + return reset($this->items); + } + + /** @param mixed $offset */ + public function offsetExists($offset): bool + { + return isset($this->items[$offset]); + } + + /** @param mixed $offset */ + public function offsetGet($offset): ?DateTimeInterface + { + return $this->items[$offset] ?? null; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value): void + { + assert($value instanceof DateTimeInterface); + if ($offset === null) { // append + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** @param mixed $offset */ + public function offsetUnset($offset): void + { + unset($this->items[$offset]); + } + + /** @return DateTimeInterface[] */ + public function jsonSerialize(): array + { + return $this->items; + } +} + +class Foo +{ + + public function doFoo() + { + $container = (new MyContainer())->add(new \DateTimeImmutable()); + + foreach ($container as $k => $item) { + assertType('int', $k); + assertType(DateTimeInterface::class, $item); + } + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6001.php b/tests/PHPStan/Analyser/nsrt/bug-6001.php new file mode 100644 index 0000000000..d4ae4ea355 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6001.php @@ -0,0 +1,49 @@ +getCode()); + assertType('(int|string)', $t->getCode()); + assertType('(int|string)', (new \RuntimeException())->getCode()); + assertType('int', (new \LogicException())->getCode()); + assertType('(int|string)', (new \PDOException())->getCode()); + assertType('int', (new MyException())->getCode()); + assertType('(int|string)', (new SubPDOException())->getCode()); + assertType('1|2|3', (new ExceptionWithMethodTag())->getCode()); + } + + /** + * @param \PDOException|MyException $exception + * @return void + */ + public function doBar($exception): void + { + assertType('(int|string)', $exception->getCode()); + } + +} + +class MyException extends \Exception +{ + +} + +class SubPDOException extends \PDOException +{ + +} + +/** + * @method 1|2|3 getCode() + */ +class ExceptionWithMethodTag extends \Exception +{ + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6070.php b/tests/PHPStan/Analyser/nsrt/bug-6070.php new file mode 100644 index 0000000000..aa0d318458 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6070.php @@ -0,0 +1,23 @@ +>', $nonEmptyArray); + + return $nonEmptyArray; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6108.php b/tests/PHPStan/Analyser/nsrt/bug-6108.php new file mode 100644 index 0000000000..e19760df07 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6108.php @@ -0,0 +1,38 @@ + [1, 2], + 'b' => [3, 4, 5], + 'c' => true, + ]; + } + + function doBar() + { + $x = $this->doFoo(); + $test = ['a' => true, 'b' => false]; + foreach ($test as $key => $value) { + if ($value) { + assertType('\'a\'|\'b\'', $key); // could be just 'a' + assertType('array', $x[$key]); + } + } + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6174.php b/tests/PHPStan/Analyser/nsrt/bug-6174.php new file mode 100644 index 0000000000..08dcc48747 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6174.php @@ -0,0 +1,21 @@ +returnValue() ?? self::DEFAULT_VALUE); + assertType('-1|int<1, max>', $tempValue === -1 || $tempValue > 0 ? $tempValue : self::DEFAULT_VALUE); + } + + public function returnValue(): ?string + { + return null; + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6293.php b/tests/PHPStan/Analyser/nsrt/bug-6293.php new file mode 100644 index 0000000000..993f7b470e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6293.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug6239; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class UnionWithNullFails +{ + /** + * @param int|null|bool $value + */ + public function withPhpDoc(mixed $value): void + { + assertType('bool|int|null', $value); + assertNativeType('mixed', $value); + } + + public function doFoo(): void + { + $this->withPhpDoc(null); + } +} 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/nsrt/bug-6305.php b/tests/PHPStan/Analyser/nsrt/bug-6305.php new file mode 100644 index 0000000000..89bfea9c62 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6305.php @@ -0,0 +1,19 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug6308; + +use function PHPStan\Testing\assertType; + +class BaseFinderStatic +{ + static public function find(): false|static + { + return false; + } +} + +final class UnionStaticStrict extends BaseFinderStatic +{ + public function something() + { + assertType('Bug6308\UnionStaticStrict|false', $this->find()); + } +} + +class UnionStaticStrict2 extends BaseFinderStatic +{ + public function something() + { + assertType('static(Bug6308\UnionStaticStrict2)|false', $this->find()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6329.php b/tests/PHPStan/Analyser/nsrt/bug-6329.php new file mode 100644 index 0000000000..b31842e05f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6329.php @@ -0,0 +1,185 @@ + 0 || null === $a) { + assertType('non-empty-string|null', $a); + } + + if (null === $a || is_string($a) && strlen($a) > 0) { + assertType('non-empty-string|null', $a); + } +} + + +/** + * @param mixed $a + */ +function int1($a): void +{ + if (is_int($a) && 0 !== $a || null === $a) { + assertType('int|int<1, max>|null', $a); + } + + if (0 !== $a && is_int($a) || null === $a) { + assertType('int|int<1, max>|null', $a); + } + + if (null === $a || is_int($a) && 0 !== $a) { + assertType('int|int<1, max>|null', $a); + } + + if (null === $a || 0 !== $a && is_int($a)) { + assertType('int|int<1, max>|null', $a); + } +} + +/** + * @param mixed $a + */ +function int2($a): void +{ + if (is_int($a) && $a > 0 || null === $a) { + assertType('int<1, max>|null', $a); + } + + if (null === $a || is_int($a) && $a > 0) { + assertType('int<1, max>|null', $a); + } +} + + +/** + * @param mixed $a + */ +function true($a): void +{ + if (is_bool($a) && false !== $a || null === $a) { + assertType('true|null', $a); + } + + if (false !== $a && is_bool($a) || null === $a) { + assertType('true|null', $a); + } + + if (null === $a || is_bool($a) && false !== $a) { + assertType('true|null', $a); + } + + if (null === $a || false !== $a && is_bool($a)) { + assertType('true|null', $a); + } +} + +/** + * @param mixed $a + */ +function nonEmptyArray1($a): void +{ + if (is_array($a) && [] !== $a || null === $a) { + assertType('non-empty-array|null', $a); + } + + if ([] !== $a && is_array($a) || null === $a) { + assertType('non-empty-array|null', $a); + } + + if (null === $a || is_array($a) && [] !== $a) { + assertType('non-empty-array|null', $a); + } + + if (null === $a || [] !== $a && is_array($a)) { + assertType('non-empty-array|null', $a); + } +} + +/** + * @param mixed $a + */ +function nonEmptyArray2($a): void +{ + if (is_array($a) && count($a) > 0 || null === $a) { + assertType('non-empty-array|null', $a); + } + + if (null === $a || is_array($a) && count($a) > 0) { + assertType('non-empty-array|null', $a); + } +} + +/** + * @param mixed $a + * @param mixed $b + * @param mixed $c + */ +function inverse($a, $b, $c): void +{ + if ((!is_string($a) || '' === $a) && null !== $a) { + } else { + assertType('non-empty-string|null', $a); + } + + if ((!is_int($b) || $b <= 0) && null !== $b) { + } else { + assertType('int<1, max>|null', $b); + } + + if (null !== $c && (!is_array($c) || count($c) <= 0)) { + } else { + assertType('non-empty-array|null', $c); + } +} + +/** + * @param mixed $a + * @param mixed $b + * @param mixed $c + * @param mixed $d + */ +function combinations($a, $b, $c, $d): void +{ + if (is_string($a) && '' !== $a || is_int($a) && $a > 0 || null === $a) { + assertType('int<1, max>|non-empty-string|null', $a); + } + if ((!is_string($b) || '' === $b) && (!is_int($b) || $b <= 0) && null !== $b) { + } else { + assertType('int<1, max>|non-empty-string|null', $b); + } + + if (is_array($c) && $c === array_filter($c, 'is_string', ARRAY_FILTER_USE_KEY) || null === $c) { + assertType('array|null', $c); + } + if ((!is_array($d) || $d !== array_filter($d, 'is_string', ARRAY_FILTER_USE_KEY)) && null !== $d) { + } else { + assertType('array|null', $d); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6399.php b/tests/PHPStan/Analyser/nsrt/bug-6399.php new file mode 100644 index 0000000000..50de3ae3f1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6399.php @@ -0,0 +1,60 @@ +>|null + */ + private static $threadLocalStorage = null; + + final public function __destruct(){ + assertType('ArrayObject>|null', self::$threadLocalStorage); + if(self::$threadLocalStorage !== null){ + assertType('ArrayObject>', self::$threadLocalStorage); + if (isset(self::$threadLocalStorage[$h = spl_object_id($this)])) { + assertType('ArrayObject>', self::$threadLocalStorage); + unset(self::$threadLocalStorage[$h]); + assertType('ArrayObject>', self::$threadLocalStorage); + if(self::$threadLocalStorage->count() === 0){ + self::$threadLocalStorage = null; + } + } + } + } + + public function doFoo(): void + { + if(self::$threadLocalStorage === null) { + return; + } + + assertType('ArrayObject>', self::$threadLocalStorage); + if (isset(self::$threadLocalStorage[1])) { + assertType('ArrayObject>&hasOffset(1)', self::$threadLocalStorage); + } else { + assertType('ArrayObject>', self::$threadLocalStorage); + } + + assertType('ArrayObject>', self::$threadLocalStorage); + if (isset(self::$threadLocalStorage[1]) && isset(self::$threadLocalStorage[2])) { + assertType('ArrayObject>&hasOffset(1)&hasOffset(2)', self::$threadLocalStorage); + unset(self::$threadLocalStorage[2]); + assertType('ArrayObject>&hasOffset(1)', self::$threadLocalStorage); + } + } + + /** + * @param non-empty-array $a + * @return void + */ + public function doBar(array $a): void + { + assertType('non-empty-array', $a); + unset($a[1]); + assertType('array', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6404.php b/tests/PHPStan/Analyser/nsrt/bug-6404.php new file mode 100644 index 0000000000..422c6e84f1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6404.php @@ -0,0 +1,85 @@ + + */ + private $someMap = []; + + public function build(): void + { + foreach (self::FOOS as $fooClass) { + if (is_a($fooClass, Foo::class, true)) { + assertType("'Bug6404\\\\Foo'", $fooClass); + assertType('int', $fooClass::getCode()); + $this->someMap[$fooClass::getCode()] = true; + } + } + } + + /** + * @param object[] $objects + * @return void + */ + public function build2(array $objects): void + { + foreach ($objects as $fooClass) { + if (is_a($fooClass, Foo::class)) { + assertType(Foo::class, $fooClass); + assertType('int', $fooClass::getCode()); + $this->someMap[$fooClass::getCode()] = true; + } + } + } + + /** + * @param mixed[] $mixeds + * @return void + */ + public function build3(array $mixeds): void + { + foreach ($mixeds as $fooClass) { + if (is_a($fooClass, Foo::class, true)) { + assertType('Bug6404\\Foo|class-string', $fooClass); + assertType('int', $fooClass::getCode()); + $this->someMap[$fooClass::getCode()] = true; + } + } + } + + /** + * @param class-string $classString + * @return void + */ + public function doBar(string $classString): void + { + assertType("class-string<" . Foo::class . ">", $classString); + assertType('int', $classString::getCode()); + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->someMap; + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6488.php b/tests/PHPStan/Analyser/nsrt/bug-6488.php new file mode 100644 index 0000000000..7f66763d2d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6488.php @@ -0,0 +1,26 @@ + $value) { + if ($value % 2 === 0) { + unset($items[$key]); + } + } + + assertType('bool',sizeof($items) > 0); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6497.php b/tests/PHPStan/Analyser/nsrt/bug-6497.php new file mode 100644 index 0000000000..23d08e4e6b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6497.php @@ -0,0 +1,18 @@ + */ + $array = [ + ['foo' => 'baz', 'bar' => 3], + ]; + + $array2 = array_column($array, null, 'foo'); + + assertType('array', $array2); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6500.php b/tests/PHPStan/Analyser/nsrt/bug-6500.php new file mode 100644 index 0000000000..d6a1be8fc1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6500.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug6505; + +use function PHPStan\Testing\assertType; + +/** @template T */ +interface Type +{ + /** + * @param T $val + * @return T + */ + public function validate($val); +} + +/** + * @template T + * @implements Type> + */ +final class ClassStringType implements Type +{ + /** @param class-string $classString */ + public function __construct(public string $classString) + { + } + + public function validate($val) { + return $val; + } +} + +/** + * @implements Type> + */ +final class StdClassType implements Type +{ + public function validate($val) { + return $val; + } +} + + +/** + * @template T + * @implements Type + */ +final class TypeCollection implements Type +{ + /** @param Type $type */ + public function __construct(public Type $type) + { + } + public function validate($val) { + return $val; + } +} + +class Foo +{ + + public function doFoo() + { + $c = new TypeCollection(new ClassStringType(\stdClass::class)); + assertType('array>', $c->validate([\stdClass::class])); + $c2 = new TypeCollection(new StdClassType()); + assertType('array>', $c2->validate([\stdClass::class])); + } + + /** + * @template T + * @param T $t + * @return T + */ + function unbounded($t) { + return $t; + } + + /** + * @template T of string + * @param T $t + * @return T + */ + function bounded1($t) { + return $t; + } + + /** + * @template T of object|class-string + * @param T $t + * @return T + */ + function bounded2($t) { + return $t; + } + + /** @param class-string<\stdClass> $p */ + function test($p): void { + assertType('class-string', $this->unbounded($p)); + assertType('class-string', $this->bounded1($p)); + assertType('class-string', $this->bounded2($p)); + } + +} + +/** + * @template TKey of array-key + * @template TValue + */ +class Collection +{ + /** + * @var array + */ + protected array $items; + + /** + * Create a new collection. + * + * @param array|null $items + * @return void + */ + public function __construct(?array $items = []) + { + $this->items = $items ?? []; + } +} + +class Example +{ + /** @var array> */ + private array $factories = []; + + public function getFactories(): void + { + assertType('Bug6505\Collection>', new Collection($this->factories)); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/bug-651.php b/tests/PHPStan/Analyser/nsrt/bug-651.php new file mode 100644 index 0000000000..7a00030f04 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-651.php @@ -0,0 +1,27 @@ +' . $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/nsrt/bug-6566-types.php b/tests/PHPStan/Analyser/nsrt/bug-6566-types.php new file mode 100644 index 0000000000..199f9d2a03 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6566-types.php @@ -0,0 +1,49 @@ += 8.0 + +namespace Bug6566Types; + +use function PHPStan\Testing\assertType; + +class A { + public string $name; +} + +class B { + public string $name; +} + +class C { + +} + +/** + * @template T of A|B|C + */ +abstract class HelloWorld +{ + public function sayHelloBug(): void + { + $object = $this->getObject(); + assertType('T of Bug6566Types\A|Bug6566Types\B|Bug6566Types\C (class Bug6566Types\HelloWorld, argument)', $object); + if ($object instanceof C) { + assertType('T of Bug6566Types\C (class Bug6566Types\HelloWorld, argument)', $object); + return; + } + assertType('T of Bug6566Types\A|Bug6566Types\B (class Bug6566Types\HelloWorld, argument)', $object); + if ($object instanceof B) { + assertType('T of Bug6566Types\B (class Bug6566Types\HelloWorld, argument)', $object); + return; + } + assertType('T of Bug6566Types\A (class Bug6566Types\HelloWorld, argument)', $object); + if ($object instanceof A) { + assertType('T of Bug6566Types\A (class Bug6566Types\HelloWorld, argument)', $object); + return; + } + assertType('*NEVER*', $object); + } + + /** + * @return T + */ + abstract protected function getObject(): A|B|C; +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6584.php b/tests/PHPStan/Analyser/nsrt/bug-6584.php new file mode 100644 index 0000000000..c989a35199 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6584.php @@ -0,0 +1,44 @@ +same($int)); + assertType('int', $this->sameWithDefault($int)); + + assertType('int|null', $this->same($intOrNull)); + assertType('int|null', $this->sameWithDefault($intOrNull)); + + assertType('null', $this->same(null)); + assertType('null', $this->sameWithDefault(null)); + assertType('null', $this->sameWithDefault()); + } + + + /** + * @template T + * @param T $t + * @return T + */ + function same($t) { + assertType('T (method Bug6584\Foo::same(), argument)', $t); + return $t; + } + + /** + * @template T + * @param T $t + * @return T + */ + function sameWithDefault($t = null) { + assertType('T (method Bug6584\Foo::sameWithDefault(), argument)', $t); + return $t; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6591.php b/tests/PHPStan/Analyser/nsrt/bug-6591.php new file mode 100644 index 0000000000..01ddac64cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6591.php @@ -0,0 +1,74 @@ + + */ + public function extract(object $object): array; +} + +interface EntityInterface { + public const IDENTITY = 'identity'; + public const CREATED = 'created'; + public function getIdentity(): string; + public function getCreated(): \DateTimeImmutable; +} +interface UpdatableInterface extends EntityInterface { + public const UPDATED = 'updated'; + public function getUpdated(): \DateTimeImmutable; + public function setUpdated(\DateTimeImmutable $updated): void; +} +interface EnableableInterface extends UpdatableInterface { + public const ENABLED = 'enabled'; + public function isEnabled(): bool; + public function setEnabled(bool $enabled): void; +} + + +/** + * @template T of EntityInterface + */ +class DoctrineEntityHydrator implements HydratorInterface +{ + /** @param T $object */ + public function extract(object $object): array + { + $data = [ + EntityInterface::IDENTITY => $object->getIdentity(), + EntityInterface::CREATED => $object->getCreated()->format('c'), + ]; + assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + if ($object instanceof UpdatableInterface) { + assertType('Bug6591\UpdatableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + $data[UpdatableInterface::UPDATED] = $object->getUpdated()->format('c'); + } else { + assertType('T of Bug6591\EntityInterface~Bug6591\UpdatableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + } + + assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + + if ($object instanceof EnableableInterface) { + assertType('Bug6591\EnableableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + $data[EnableableInterface::ENABLED] = $object->isEnabled(); + } else { + assertType('T of Bug6591\EntityInterface~Bug6591\EnableableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + } + + assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + + return [...$data, ...$this->performExtraction($object)]; + } + + /** + * @param T $entity + * @return array + */ + public function performExtraction(EntityInterface $entity): array + { + return []; + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-6624.php b/tests/PHPStan/Analyser/nsrt/bug-6624.php new file mode 100644 index 0000000000..bbdf3b9395 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6624.php @@ -0,0 +1,31 @@ +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/nsrt/bug-6672.php b/tests/PHPStan/Analyser/nsrt/bug-6672.php new file mode 100644 index 0000000000..1ae5b9c0f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6672.php @@ -0,0 +1,44 @@ + 17) { + assertType('int<18, max>', $a); + } else { + assertType('int', $a); + } + + if ($b > 17 || $b === null) { + assertType('int<18, max>|null', $b); + } else { + assertType('int', $b); + } + + if ($c < 17) { + assertType('int', $c); + } else { + assertType('int<17, max>', $c); + } + + if ($d < 17 || $d === null) { + assertType('int|null', $d); + } else { + assertType('int<17, max>', $d); + } + + if ($e >= 17 && $e <= 19 || $e === null) { + assertType('int<17, 19>|null', $e); + } else { + assertType('int|int<20, max>', $e); + } + + if ($f < 17 || $f > 19 || $f === null) { + assertType('int|int<20, max>|null', $f); + } else { + assertType('int<17, 19>', $f); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6682.php b/tests/PHPStan/Analyser/nsrt/bug-6682.php new file mode 100644 index 0000000000..cdb2738ceb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6682.php @@ -0,0 +1,19 @@ +|null>> $data + */ + public function __construct(array $data) + { + $x = array_column($data, null, 'type'); + assertType('array|string|null>>', $x); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6687.php b/tests/PHPStan/Analyser/nsrt/bug-6687.php new file mode 100644 index 0000000000..77ee0f940a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6687.php @@ -0,0 +1,35 @@ +', $a); + } + } + + function bar(string $a): void + { + if ($a === 'FOO' || is_subclass_of($a, 'FOO')) { + assertType('class-string', $a); + } + } + + function baz(string $a): void + { + if ($a === BAZ || is_subclass_of($a, BAZ)) { + assertType("'BAZ'|class-string", $a); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6695.php b/tests/PHPStan/Analyser/nsrt/bug-6695.php new file mode 100644 index 0000000000..396548a4aa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6695.php @@ -0,0 +1,58 @@ += 8.1 + +namespace Bug6695; + +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + case BAR = 1; + case BAZ = 2; + + public function toCollection(): void + { + assertType('Bug6695\Collection', $this->collect(self::cases())); + } + + /** + * Create a collection from the given value. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $value + * @return Collection + */ + function collect($value): Collection + { + return new Collection($value); + } + +} + +/** + * @template TKey of array-key + * @template TValue + * + */ +class Collection +{ + + /** + * The items contained in the collection. + * + * @var iterable + */ + protected $items = []; + + /** + * Create a new collection. + * + * @param iterable $items + * @return void + */ + public function __construct($items = []) + { + $this->items = $items; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6696.php b/tests/PHPStan/Analyser/nsrt/bug-6696.php new file mode 100644 index 0000000000..6e4dd96666 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6696.php @@ -0,0 +1,20 @@ + + */ + public function getClasses(): iterable; +} + +class Y +{ + /** @var X */ + public $x; + + /** + * @template T of object + * + * @param class-string $type + * @return iterable> + */ + public function findImplementations(string $type): iterable + { + foreach ($this->x->getClasses() as $class) { + if (is_subclass_of($class, $type)) { + assertType('class-string', $class); + yield $class; + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6699.php b/tests/PHPStan/Analyser/nsrt/bug-6699.php new file mode 100644 index 0000000000..6e5bad05c4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6699.php @@ -0,0 +1,30 @@ +value = $value; + } + + /** + * @param class-string<\Exception> $exceptionClass + * @return void + */ + public function doFoo(string $exceptionClass) + { + assertType('class-string', (new Foo($exceptionClass))->value); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6704.php b/tests/PHPStan/Analyser/nsrt/bug-6704.php new file mode 100644 index 0000000000..f342fed93a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6704.php @@ -0,0 +1,22 @@ +|class-string $a + * @param DateTimeImmutable|stdClass $b + */ +function foo(string $a, object $b): void +{ + if (!is_a($a, stdClass::class, true)) { + assertType('class-string', $a); + } + + if (!is_a($b, stdClass::class)) { + assertType('DateTimeImmutable', $b); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6715.php b/tests/PHPStan/Analyser/nsrt/bug-6715.php new file mode 100644 index 0000000000..1916bea3be --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6715.php @@ -0,0 +1,49 @@ + true, + ])); + + assertType('array{}', array_filter([ + 'test' => false, + ])); + + assertType('array{test?: 1}', array_filter([ + 'test' => rand(0, 1), + ])); + + assertType('array{test?: true}', array_filter([ + 'test' => $this->bool, + ])); + } + + function test2(): void + { + assertType('\'1\'', implode(', ', array_filter([ + 'test' => true, + ]))); + + assertType('\'\'', implode(', ', array_filter([ + 'test' => false, + ]))); + + assertType('\'\'|\'1\'', implode(', ', array_filter([ + 'test' => rand(0, 1), + ]))); + + assertType('\'\'|\'1\'', implode(', ', array_filter([ + 'test' => $this->bool, + ]))); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/bug-778.php b/tests/PHPStan/Analyser/nsrt/bug-778.php new file mode 100644 index 0000000000..0d561b2836 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-778.php @@ -0,0 +1,112 @@ +baz(); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $handle); + } + } + + public function lorem(): void + { + try { + $foo = foo(); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + + public function lorem2(): void + { + try { + $foo = foo(); + } finally { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + + public function lorem3(): void + { + try { + $foo = foo(); + } catch (\Exception $e) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + + public function lorem4(): void + { + try { + $foo = foo(); + } catch (\Exception $e) { + + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + public function ipsum(): void + { + try { + doFoo(); + } catch (\InvalidArgumentException $e) { + + } catch (\RuntimeException $e) { + + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $e); + assertType('InvalidArgumentException|RuntimeException', $e); + } + + public function dolor(): void + { + try { + doFoo(); + } catch (\InvalidArgumentException $e) { + + } catch (\RuntimeException $e) { + + } finally { + + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $e); + assertType('InvalidArgumentException|RuntimeException', $e); + } + + public function sit(): void + { + try { + $var = 1; + } catch (\Exception $e) { + + } + + assertVariableCertainty(TrinaryLogic::createNo(), $e); + } +} 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/nsrt/bug-801.php b/tests/PHPStan/Analyser/nsrt/bug-801.php new file mode 100644 index 0000000000..e349376162 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-801.php @@ -0,0 +1,26 @@ +getDateTime(); + + $condition = null !== $dt; + + if ($condition) { + assertType('DateTime', $dt); + } else { + assertType('null', $dt); + } +}; 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-8592.php b/tests/PHPStan/Analyser/nsrt/bug-8592.php new file mode 100644 index 0000000000..e876597853 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8592.php @@ -0,0 +1,15 @@ + $foo + */ +function foo(array $foo): void +{ + foreach ($foo as $key => $value) { + assertType('int|numeric-string', $key); + } +} 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/nsrt/bug-987.php b/tests/PHPStan/Analyser/nsrt/bug-987.php new file mode 100644 index 0000000000..c168ef6224 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-987.php @@ -0,0 +1,53 @@ +arrOrNull(); + if ($boolOption && $myArray === null) { + throw new \Exception(''); + } + if (!$boolOption) { + $myArray = $class->otherGetArray(); + } + + assertType('array', $myArray); + + return $myArray; + } + +} 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/nsrt/bug-empty-array.php b/tests/PHPStan/Analyser/nsrt/bug-empty-array.php new file mode 100644 index 0000000000..91a6b8bb00 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-empty-array.php @@ -0,0 +1,43 @@ +', $this->comments); + $this->comments = []; + assertType('array{}', $this->comments); + if ($this->comments === []) { + assertType('array{}', $this->comments); + return; + } else { + assertType('*NEVER*', $this->comments); + } + + assertType('*NEVER*', $this->comments); + } + + public function doBar(): void + { + assertType('array', $this->comments); + $this->comments = []; + assertType('array{}', $this->comments); + if ([] === $this->comments) { + assertType('array{}', $this->comments); + return; + } else { + assertType('*NEVER*', $this->comments); + } + + assertType('*NEVER*', $this->comments); + } + +} 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/nsrt/bug-pr-339.php b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php new file mode 100644 index 0000000000..4d841ad783 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php @@ -0,0 +1,33 @@ +__toString()); + assertNativeType('mixed', $test->__toString()); +} 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 93% rename from tests/PHPStan/Analyser/data/bug2574.php rename to tests/PHPStan/Analyser/nsrt/bug2574.php index 7ac0afdbf9..711a4077a4 100644 --- a/tests/PHPStan/Analyser/data/bug2574.php +++ b/tests/PHPStan/Analyser/nsrt/bug2574.php @@ -2,7 +2,7 @@ namespace Analyser\Bug2574; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; abstract class Model { /** @return static */ diff --git a/tests/PHPStan/Analyser/data/bug2577.php b/tests/PHPStan/Analyser/nsrt/bug2577.php similarity index 91% rename from tests/PHPStan/Analyser/data/bug2577.php rename to tests/PHPStan/Analyser/nsrt/bug2577.php index fe722a5664..843d242984 100644 --- a/tests/PHPStan/Analyser/data/bug2577.php +++ b/tests/PHPStan/Analyser/nsrt/bug2577.php @@ -2,7 +2,7 @@ namespace Analyser\Bug2577; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class A {} class A1 extends A {} 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/nsrt/catch-without-variable.php b/tests/PHPStan/Analyser/nsrt/catch-without-variable.php new file mode 100644 index 0000000000..0ac5a4110a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/catch-without-variable.php @@ -0,0 +1,24 @@ +test(); + } catch (\FooException) { + assertType('*ERROR*', $e); + } + } + +} 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/nsrt/class-constant-types.php b/tests/PHPStan/Analyser/nsrt/class-constant-types.php new file mode 100644 index 0000000000..8f19f7659e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-constant-types.php @@ -0,0 +1,118 @@ += 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/nsrt/class-reflection-interfaces.php b/tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php new file mode 100644 index 0000000000..eac3ea9b0a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php @@ -0,0 +1,26 @@ + + */ +interface ResultStatement extends \Traversable +{ + +} + +interface Statement extends ResultStatement +{ + +} + +function (Statement $s): void +{ + foreach ($s as $k => $v) { + assertType('int', $k); + assertType('mixed', $v); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php new file mode 100644 index 0000000000..4e2429e6dc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php @@ -0,0 +1,30 @@ +base); + assertType('int', $this->foo); + assertType('int', $this->bar); + assertType('*NEVER*', $this->baz); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/classPhpDocs.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php new file mode 100644 index 0000000000..f0024022ce --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php @@ -0,0 +1,45 @@ + arrayOfStrings() + * @phpstan-method array arrayOfInts() + * @phan-method array arrayOfStrings() + * @method array arrayOfInts() + * @method mixed overrodeMethod() + * @method static mixed overrodeStaticMethod() + */ +class Foo +{ + public function __call($name, $arguments){} + + public static function __callStatic($name, $arguments){} + + public function doFoo() + { + assertType('string', $this->string()); + assertType('array', $this->arrayOfStrings()); + assertType('array', $this->arrayOfInts()); + assertType('mixed', $this->overrodeMethod()); + assertType('mixed', static::overrodeStaticMethod()); + } +} + +/** + * @phpstan-method string overrodeMethod() + * @phpstan-method static int overrodeStaticMethod() + */ +class Child extends Foo +{ + public function doFoo() + { + assertType('string', $this->overrodeMethod()); + assertType('int', static::overrodeStaticMethod()); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/clear-stat-cache.php b/tests/PHPStan/Analyser/nsrt/clear-stat-cache.php new file mode 100644 index 0000000000..48686cf61d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/clear-stat-cache.php @@ -0,0 +1,62 @@ +', $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 @@ +bindTo($newThis); +assertType('Closure(object): true', $boundClosure); + +$staticallyBoundClosure = \Closure::bind($closure, $newThis); +assertType('Closure(object): true', $staticallyBoundClosure); + +$returnType = $closure->call($newThis, new class {}); +assertType('true', $returnType); + +$staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis); +assertType('Closure(object): true', $staticallyBoundClosureCaseInsensitive); diff --git a/tests/PHPStan/Analyser/nsrt/closure-return-type.php b/tests/PHPStan/Analyser/nsrt/closure-return-type.php new file mode 100644 index 0000000000..386fec990c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-return-type.php @@ -0,0 +1,159 @@ + 'bar']; + }; + assertType('array{foo: \'bar\'}', $f()); + + $f = function (string $s) { + return $s; + }; + assertType('string', $f('foo')); + + $f = function () use ($i) { + return $i; + }; + assertType('int', $f()); + + $f = function () use ($i) { + if (rand(0, 1)) { + return $i; + } + + return null; + }; + assertType('int|null', $f()); + + $f = function () use ($i) { + if (rand(0, 1)) { + return $i; + } + + return; + }; + assertType('int|null', $f()); + + $f = function () { + yield 1; + return 2; + }; + assertType('Generator', $f()); + + $g = function () use ($f) { + yield from $f(); + }; + assertType('Generator', $g()); + + $h = function (): \Generator { + yield 1; + return 2; + }; + assertType('Generator', $h()); + } + + public function doBar(): void + { + $f = function () { + if (rand(0, 1)) { + return 1; + } + + function () { + return 'foo'; + }; + + $c = new class() { + public function doFoo() { + return 2.0; + } + }; + + return 2; + }; + + assertType('1|2', $f()); + } + + /** + * @return never + */ + public function returnNever(): void + { + + } + + public function doBaz(): void + { + $f = function() { + $this->returnNever(); + }; + assertType('never', $f()); + + $f = function(): void { + $this->returnNever(); + }; + assertType('never', $f()); + + $f = function() { + if (rand(0, 1)) { + return; + } + + $this->returnNever(); + }; + assertType('void', $f()); + + $f = function(array $a) { + foreach ($a as $v) { + continue; + } + + $this->returnNever(); + }; + assertType('never', $f([])); + + $f = function(array $a) { + foreach ($a as $v) { + $this->returnNever(); + } + }; + assertType('void', $f([])); + + $f = function() { + foreach ([1, 2, 3] as $v) { + $this->returnNever(); + } + }; + assertType('never', $f()); + + $f = function (): \stdClass { + throw new \Exception(); + }; + 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/nsrt/compact.php b/tests/PHPStan/Analyser/nsrt/compact.php new file mode 100644 index 0000000000..b15f2f5eb4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/compact.php @@ -0,0 +1,22 @@ + 'bar'])); + +function (string $dolor): void { + $foo = 'bar'; + $bar = 'baz'; + if (rand(0, 1)) { + $lorem = 'ipsum'; + } + assertType('array{foo: \'bar\', bar: \'baz\'}', compact('foo', ['bar'])); + assertType('array{foo: \'bar\', bar: \'baz\', lorem?: \'ipsum\'}', compact([['foo']], 'bar', 'lorem')); + + assertType('array', compact($dolor)); + assertType('array', compact([$dolor])); + + assertType('array{}', compact([])); +}; diff --git a/tests/PHPStan/Analyser/nsrt/comparison-operators.php b/tests/PHPStan/Analyser/nsrt/comparison-operators.php new file mode 100644 index 0000000000..14488df983 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/comparison-operators.php @@ -0,0 +1,359 @@ + false); + assertType('true', true >= false); + assertType('false', true < false); + assertType('false', true <= false); + assertType('false', false > true); + assertType('false', false >= true); + assertType('true', false < true); + assertType('true', false <= true); + } + + public function string(): void + { + assertType('false', 'foo' < 'bar'); + assertType('false', 'foo' <= 'bar'); + assertType('true', 'foo' > 'bar'); + assertType('true', 'foo' >= 'bar'); + } + + public function float(): void + { + assertType('true', 1.9 > 1); + assertType('true', '1.9' > 1); + + assertType('false', 1.9 > 2.1); + assertType('true', 1.9 > 1.5); + assertType('true', 1.9 < 2.1); + assertType('false', 1.9 < 1.5); + } + + public function unions(int $a, int $b): void + { + if (($a === 17 || $a === 42) && ($b === 3 || $b === 7)) { + assertType('false', $a < $b); + assertType('true', $a > $b); + assertType('false', $a <= $b); + assertType('true', $a >= $b); + } + if (($a === 11 || $a === 42) && ($b === 3 || $b === 11)) { + assertType('false', $a < $b); + assertType('bool', $a > $b); + assertType('bool', $a <= $b); + assertType('true', $a >= $b); + } + } + + public function ranges(int $a, int $b): void + { + if ($a >= 10 && $a <= 20) { + if ($b >= 30 && $b <= 40) { + assertType('true', $a < $b); + assertType('false', $a > $b); + assertType('true', $a <= $b); + assertType('false', $a >= $b); + } + } + if ($a >= 10 && $a <= 25) { + if ($b >= 25 && $b <= 40) { + assertType('bool', $a < $b); + assertType('false', $a > $b); + assertType('true', $a <= $b); + assertType('bool', $a >= $b); + } + } + } +} + +class ComparisonOperatorsInTypeSpecifier +{ + + public function null(?int $i, ?float $f, ?string $s, ?bool $b): void + { + if ($i > null) { + assertType('int|int<1, max>', $i); + } + if ($i >= null) { + assertType('int|null', $i); + } + if ($i < null) { + assertType('*NEVER*', $i); + } + if ($i <= null) { + assertType('0|null', $i); + } + + if ($f > null) { + assertType('float', $f); + } + if ($f >= null) { + assertType('float|null', $f); + } + if ($f < null) { + assertType('*NEVER*', $f); + } + if ($f <= null) { + assertType('0.0|null', $f); + } + + if ($s > null) { + assertType('non-empty-string', $s); + } + if ($s >= null) { + assertType('string|null', $s); + } + if ($s < null) { + assertType('*NEVER*', $s); + } + if ($s <= null) { + assertType('\'\'|null', $s); + } + + if ($b > null) { + assertType('true', $b); + } + if ($b >= null) { + assertType('bool|null', $b); + } + if ($b < null) { + assertType('*NEVER*', $b); + } + if ($b <= null) { + assertType('false|null', $b); + } + } + + public function bool(?bool $b): void + { + if ($b > false) { + assertType('true', $b); + } + if ($b >= false) { + assertType('bool|null', $b); + } + if ($b < false) { + assertType('*NEVER*', $b); + } + if ($b <= false) { + assertType('false|null', $b); + } + + if ($b > true) { + assertType('*NEVER*', $b); + } + if ($b >= true) { + assertType('true', $b); + } + if ($b < true) { + assertType('false|null', $b); + } + if ($b <= true) { + assertType('bool|null', $b); + } + } + + public function string(?string $s): void + { + if ($s < '') { + assertType('*NEVER*', $s); + } + if ($s <= '') { + assertType('string|null', $s); // Would be nice to have ''|null + } + } + + public function intPositive10(?int $i, ?float $f): void + { + if ($i > 10) { + assertType('int<11, max>', $i); + } + if ($i >= 10) { + assertType('int<10, max>', $i); + } + if ($i < 10) { + assertType('int|null', $i); + } + if ($i <= 10) { + assertType('int|null', $i); + } + + if ($f > 10) { + assertType('float', $f); + } + if ($f >= 10) { + assertType('float', $f); + } + if ($f < 10) { + assertType('float|null', $f); + } + if ($f <= 10) { + assertType('float|null', $f); + } + } + + public function intNegative10(?int $i, ?float $f): void + { + if ($i > -10) { + assertType('int<-9, max>', $i); + } + if ($i >= -10) { + assertType('int<-10, max>', $i); + } + if ($i < -10) { + assertType('int|null', $i); + } + if ($i <= -10) { + assertType('int|null', $i); + } + + if ($f > -10) { + assertType('float', $f); + } + if ($f >= -10) { + assertType('float', $f); + } + if ($f < -10) { + assertType('float|null', $f); + } + if ($f <= -10) { + assertType('float|null', $f); + } + } + + public function intZero(?int $i, ?float $f): void + { + if ($i > 0) { + assertType('int<1, max>', $i); + } + if ($i >= 0) { + assertType('int<0, max>|null', $i); + } + if ($i < 0) { + assertType('int', $i); + } + if ($i <= 0) { + assertType('int|null', $i); + } + + if ($f > 0) { + assertType('float', $f); + } + if ($f >= 0) { + assertType('float|null', $f); + } + if ($f < 0) { + assertType('float', $f); + } + if ($f <= 0) { + assertType('float|null', $f); + } + } + + public function float10(?int $i): void + { + if ($i > 10.0) { + assertType('int<11, max>', $i); + } + if ($i >= 10.0) { + assertType('int<10, max>', $i); + } + if ($i < 10.0) { + assertType('int|null', $i); + } + if ($i <= 10.0) { + assertType('int|null', $i); + } + + if ($i > 10.1) { + assertType('int<11, max>', $i); + } + if ($i >= 10.1) { + assertType('int<11, max>', $i); + } + if ($i < 10.1) { + assertType('int|null', $i); + } + if ($i <= 10.1) { + assertType('int|null', $i); + } + } + + public function floatZero(?int $i): void + { + if ($i > 0.0) { + assertType('int<1, max>', $i); + } + if ($i >= 0.0) { + assertType('int<0, max>|null', $i); + } + if ($i < 0.0) { + assertType('int', $i); + } + if ($i <= 0.0) { + assertType('int|null', $i); + } + } + + public function ranges(int $a, ?int $b): void + { + if ($a >= 17 && $a <= 42) { + if ($b < $a) { + assertType('int|null', $b); + } + if ($b <= $a) { + assertType('int|null', $b); + } + if ($b > $a) { + assertType('int<18, max>', $b); + } + if ($b >= $a) { + assertType('int<17, max>', $b); + } + } + + if ($a >= -17 && $a <= 42) { + if ($b < $a) { + assertType('int|null', $b); + } + if ($b <= $a) { + assertType('int|null', $b); + } + if ($b > $a) { + assertType('int<-16, max>', $b); + } + if ($b >= $a) { + assertType('int<-17, max>|null', $b); + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/complex-generics-example.php b/tests/PHPStan/Analyser/nsrt/complex-generics-example.php similarity index 95% rename from tests/PHPStan/Analyser/data/complex-generics-example.php rename to tests/PHPStan/Analyser/nsrt/complex-generics-example.php index dc3b24bb34..3c18573f45 100644 --- a/tests/PHPStan/Analyser/data/complex-generics-example.php +++ b/tests/PHPStan/Analyser/nsrt/complex-generics-example.php @@ -2,7 +2,7 @@ namespace ComplexGenericsExample; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; /** * @template TVariant of VariantInterface 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/nsrt/conditional-non-empty-array.php b/tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php new file mode 100644 index 0000000000..7f3ede3f30 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php @@ -0,0 +1,30 @@ + 0) { + assertType('non-empty-array', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } else { + assertType('array{}', $a); + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } + } + +} 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/nsrt/const-expr-phpdoc-type.php b/tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php new file mode 100644 index 0000000000..ab79fb6a46 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php @@ -0,0 +1,60 @@ + $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 @@ +', $a); + + $b = [1, 2, 3]; + $b[3] = 4; + assertType('array{1, 2, 3, 4}', $b); + + $c = [false, false, false]; + /** @var 0|1|2 $offset */ + $offset = doFoo(); + $c[$offset] = true; + assertType('array{bool, bool, bool}', $c); + + $d = [false, false, false]; + /** @var int<0, 2> $offset2 */ + $offset2 = doFoo(); + $d[$offset2] = true; + assertType('array{bool, bool, bool}', $d); + + $e = [false, false, false]; + /** @var 0|1|2|3 $offset3 */ + $offset3 = doFoo(); + $e[$offset3] = true; + assertType('non-empty-array<0|1|2|3, bool>', $e); + + $f = [false, false, false]; + /** @var 0|1 $offset4 */ + $offset4 = doFoo(); + $f[$offset4] = true; + assertType('array{bool, bool, false}', $f); + } + + /** + * @param int<0, 1> $offset + * @return void + */ + public function doBar(int $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('array{bool, bool, false}', $a); + } + + /** + * @param int<0, 1>|int<3, 4> $offset + * @return void + */ + public function doBar2(int $offset): void + { + $a = [false, false, false, false, false]; + $a[$offset] = true; + assertType('array{bool, bool, false, bool, bool}', $a); + } + + /** + * @param int<0, max> $offset + * @return void + */ + public function doBar3(int $offset): void + { + $a = [false, false, false, false, false]; + $a[$offset] = true; + assertType('non-empty-array, bool>', $a); + } + + /** + * @param int $offset + * @return void + */ + public function doBar4(int $offset): void + { + $a = [false, false, false, false, false]; + $a[$offset] = true; + assertType('non-empty-array, bool>', $a); + } + + /** + * @param int<0, 4> $offset + * @return void + */ + public function doBar5(int $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('non-empty-array, bool>', $a); + } + + public function doBar6(bool $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('array{bool, bool, false}', $a); + } + + /** + * @param true $offset + */ + public function doBar7(bool $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('array{false, true, false}', $a); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-union-unshift.php b/tests/PHPStan/Analyser/nsrt/constant-array-union-unshift.php new file mode 100644 index 0000000000..b02838d86b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/constant-array-union-unshift.php @@ -0,0 +1,19 @@ + 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/nsrt/date-format.php b/tests/PHPStan/Analyser/nsrt/date-format.php new file mode 100644 index 0000000000..e8a6878521 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/date-format.php @@ -0,0 +1,49 @@ +format('')); + assertType('string', $dt->format($s)); + assertType('non-falsy-string', $dt->format('D')); + assertType('numeric-string', $dt->format('Y')); + assertType('numeric-string', $dt->format('Ghi')); +}; + +function (\DateTime $dt, string $s): void { + assertType('\'\'', $dt->format('')); + assertType('string', $dt->format($s)); + assertType('non-falsy-string', $dt->format('D')); + assertType('numeric-string', $dt->format('Y')); + assertType('numeric-string', $dt->format('Ghi')); +}; + +function (\DateTimeImmutable $dt, string $s): void { + assertType('\'\'', $dt->format('')); + assertType('string', $dt->format($s)); + assertType('non-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/nsrt/date-period-return-types.php b/tests/PHPStan/Analyser/nsrt/date-period-return-types.php new file mode 100644 index 0000000000..17a08cbd18 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/date-period-return-types.php @@ -0,0 +1,40 @@ +', $datePeriod); +assertType(\DateTime::class, $datePeriod->getEndDate()); +assertType('null', $datePeriod->getRecurrences()); +$datePeriodList[] = $datePeriod; + +foreach ($datePeriod as $k => $v) { + assertType('int', $k); + assertType('DateTime', $v); +} + +$datePeriod = new DatePeriod($start, $interval, $recurrences); +assertType(\DatePeriod::class . '', $datePeriod); +assertType('null', $datePeriod->getEndDate()); +assertType('4', $datePeriod->getRecurrences()); +$datePeriodList[] = $datePeriod; + +$datePeriod = new DatePeriod($iso); +assertType(\DatePeriod::class . '', $datePeriod); +assertType('null', $datePeriod->getEndDate()); +assertType('int', $datePeriod->getRecurrences()); +$datePeriodList[] = $datePeriod; + +/** @var DatePeriod $datePeriod */ +$datePeriod = $datePeriodList[random_int(0, 2)]; +assertType(\DatePeriod::class, $datePeriod); +assertType(\DateTimeInterface::class . '|null', $datePeriod->getEndDate()); +assertType('int|null', $datePeriod->getRecurrences()); diff --git a/tests/PHPStan/Analyser/nsrt/date.php b/tests/PHPStan/Analyser/nsrt/date.php new file mode 100644 index 0000000000..e734a3ebab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/date.php @@ -0,0 +1,34 @@ + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + + fn () => $b ? assertVariableCertainty(TrinaryLogic::createYes(), $foo) : assertVariableCertainty(TrinaryLogic::createNo(), $foo); + + fn ($b) => $b ? assertVariableCertainty(TrinaryLogic::createMaybe(), $foo) : assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + + fn ($foo) => $b ? assertVariableCertainty(TrinaryLogic::createYes(), $foo) : assertVariableCertainty(TrinaryLogic::createYes(), $foo); + + fn ($foo) => assertVariableCertainty(TrinaryLogic::createYes(), $foo); + +}; diff --git a/tests/PHPStan/Analyser/nsrt/dependent-variables-type-guard-same-as-type.php b/tests/PHPStan/Analyser/nsrt/dependent-variables-type-guard-same-as-type.php new file mode 100644 index 0000000000..db81724a63 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dependent-variables-type-guard-same-as-type.php @@ -0,0 +1,77 @@ +getArrayOrNull(); + if ($associationData === null) { + } else { + $itemsCounter = 0; + assertType('0', $itemsCounter); + assertType('Generator&iterable', $associationData); + foreach ($associationData as $row) { + $itemsCounter++; + assertType('int<1, max>', $itemsCounter); + } + + assertType('Generator', $associationData); + + assertType('int<0, max>', $itemsCounter); + } + } + + public function doBar(float $f, bool $b): void + { + if ($f !== 1.0) { + $foo = 'test'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + + if ($f !== 1.0) { + assertType('float', $f); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); // could be Yes, but float type is not subtractable + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); // could be No, but float type is not subtractable + } + + if ($f !== 2.0) { + assertType('float', $f); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + if ($f !== 1.0) { + assertType('float', $f); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + if ($b) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + +} 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/nsrt/early-termination-phpdoc.php b/tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php new file mode 100644 index 0000000000..bb013d05be --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php @@ -0,0 +1,46 @@ +doFooPhpDoc(); + } else { + $a = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +}; + +function(): void { + if (rand(0, 1)) { + bazPhpDoc(); + } else { + $a = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +}; + +function(): void { + if (rand(0, 1)) { + bazPhpDoc2(); + } else { + $a = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +}; diff --git a/tests/PHPStan/Analyser/nsrt/empty-array-shape.php b/tests/PHPStan/Analyser/nsrt/empty-array-shape.php new file mode 100644 index 0000000000..35d3dcece9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/empty-array-shape.php @@ -0,0 +1,16 @@ +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/nsrt/enums-import-alias.php b/tests/PHPStan/Analyser/nsrt/enums-import-alias.php new file mode 100644 index 0000000000..b18f7879de --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enums-import-alias.php @@ -0,0 +1,27 @@ += 8.1 + +namespace EnumTypeAssertionsImportAlias; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-import-type TypeAlias from \EnumTypeAssertions\EnumWithTypeAliases as TypeAlias2 + */ +enum Foo +{ + + /** + * @param TypeAlias2 $p + * @return TypeAlias2 + */ + public function doFoo($p) + { + assertType('array{foo: int, bar: string}', $p); + } + + public function doBar() + { + assertType('array{foo: int, bar: string}', $this->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/equal.php b/tests/PHPStan/Analyser/nsrt/equal.php new file mode 100644 index 0000000000..e91a274257 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/equal.php @@ -0,0 +1,166 @@ +|int<8, 13> $i */ + public function doBaz(int $i): void + { + assertType('int<1, 3>|int<8, 13>', $i); + if ($i == 3) { + assertType('3', $i); + } else { + assertType('int<1, 2>|int<8, 13>', $i); + } + assertType('int<1, 3>|int<8, 13>', $i); + } + + public function doLorem(float $f): void + { + assertType('float', $f); + if ($f == 3.5) { + assertType('3.5', $f); + } else { + assertType('float', $f); + } + + assertType('float', $f); + } + + public function doIpsum(array $a): void + { + assertType('array', $a); + if ($a == []) { + assertType('array{}', $a); + } else { + assertType('non-empty-array', $a); + } + assertType('array', $a); + } + + public function stdClass(\stdClass $a, \stdClass $b): void + { + if ($a == $a) { + assertType('stdClass', $a); + } else { + assertType('*NEVER*', $a); + } + + if ($b != $b) { + assertType('*NEVER*', $b); + } else { + assertType('stdClass', $b); + } + + if ($a == $b) { + assertType('stdClass', $a); + assertType('stdClass', $b); + } else { + assertType('stdClass', $a); + assertType('stdClass', $b); + } + + if ($a != $b) { + assertType('stdClass', $a); + assertType('stdClass', $b); + } else { + assertType('stdClass', $a); + assertType('stdClass', $b); + } + + assertType('stdClass', $a); + assertType('stdClass', $b); + } + + /** + * @param array{a: string, b: array{c: string|null}} $a + */ + public function arrayOffset(array $a): void + { + if (strlen($a['a']) > 0 && $a['a'] === $a['b']['c']) { + assertType('array{a: non-empty-string, b: array{c: non-empty-string}}', $a); + } + } + +} + +class Bar +{ + + public function doFoo(\stdClass $a, \stdClass $b): void + { + assertType('true', $a == $a); + assertType('bool', $a == $b); + assertType('false', $a != $a); + assertType('bool', $a != $b); + + assertType('bool', self::createStdClass() == self::createStdClass()); + assertType('bool', self::createStdClass() != self::createStdClass()); + } + + public static function createStdClass(): \stdClass + { + + } + +} + +class Baz +{ + + public function doFoo(string $a, float $c): void + { + $nullableA = $a; + if (rand(0, 1)) { + $nullableA = null; + } + + assertType('bool', $a == $nullableA); + assertType('bool', $a == 'a'); + assertType('true', 'a' == 'a'); + assertType('false', 'a' == 'b'); + + assertType('bool', $a != $nullableA); + assertType('bool', $a != 'a'); + assertType('false', 'a' != 'a'); + assertType('true', 'a' != 'b'); + + assertType('bool', $a == 1); + assertType('true', 1 == 1); + assertType('false', 1 == 0); + + assertType('bool', $c == 'a'); + assertType('bool', $c == 1); + assertType('bool', $c == 1.2); + assertType('true', 1.2 == 1.2); + assertType('false', 1.2 == 1.3); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php b/tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php new file mode 100644 index 0000000000..a95aaca46e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php @@ -0,0 +1,16 @@ +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 93% rename from tests/PHPStan/Analyser/data/ext-ds.php rename to tests/PHPStan/Analyser/nsrt/ext-ds.php index 643fcb3ece..f451971c0c 100644 --- a/tests/PHPStan/Analyser/data/ext-ds.php +++ b/tests/PHPStan/Analyser/nsrt/ext-ds.php @@ -4,7 +4,7 @@ use Ds\Map; use Ds\Set; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class A { @@ -100,10 +100,10 @@ public function setXor() : void } /** - * @implements \Iterator + * @implements \IteratorAggregate * @implements \Ds\Collection */ -abstract class Bar implements \Iterator, \Ds\Collection +abstract class Bar implements \IteratorAggregate, \Ds\Collection { public function doFoo() 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/nsrt/extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-int-types.php new file mode 100644 index 0000000000..ddf2fb868b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/extra-int-types.php @@ -0,0 +1,26 @@ +', $positiveInt); + assertType('int', $negativeInt); + assertType('false', strpos('u', $str) === -1); + assertType('true', strpos('u', $str) !== -1); + } + +} 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/nsrt/filesystem-functions.php b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php new file mode 100644 index 0000000000..fc7614c63a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php @@ -0,0 +1,68 @@ += 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 @@ += 8.1 + +namespace FirstClassCallables; + +use PHPStan\TrinaryLogic; +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; + +class Foo +{ + + public function doFoo(string $foo): void + { + assertType('Closure(string): void', $this->doFoo(...)); + assertType('Closure(): void', self::doBar(...)); + assertType('Closure', self::$foo(...)); + assertType('Closure', $this->nonexistent(...)); + assertType('Closure', $this->$foo(...)); + assertType('Closure(string): int<0, max>', strlen(...)); + assertType('Closure(string): int<0, max>', 'strlen'(...)); + assertType('Closure', 'nonexistent'(...)); + } + + public static function doBar(): void + { + + } + +} + +class GenericFoo +{ + + /** + * @template T + * @param T $a + * @return T + */ + public function doFoo($a) + { + return $a; + } + + public function doBar() + { + $f = $this->doFoo(...); + assertType('1', $f(1)); + assertType('\'foo\'', $f('foo')); + + $g = \Closure::fromCallable([$this, 'doFoo']); + assertType('1', $g(1)); + assertType('\'foo\'', $g('foo')); + } + + public function doBaz() + { + $ref = new \ReflectionClass(\stdClass::class); + assertType('class-string', $ref->getName()); + + $f = $ref->getName(...); + assertType('class-string', $f()); + + $g = \Closure::fromCallable([$ref, 'getName']); + assertType('class-string', $g()); + } + +} + +class NeverCallable +{ + + public function doFoo() + { + $n = function (): never { + throw new \Exception(); + }; + + if (rand(0, 1)) { + $n(); + } else { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + + public function doBar() + { + $n = function (): never { + throw new \Exception(); + }; + + if (rand(0, 1)) { + $n(...); + } else { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + /** + * @param callable(): never $n + */ + public function doBaz(callable $n): void + { + if (rand(0, 1)) { + $n(); + } else { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/for-loop-i-type.php b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php new file mode 100644 index 0000000000..1317b3695c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php @@ -0,0 +1,105 @@ +', $i); + } + + assertType('int<50, max>', $i); + assertType(\stdClass::class, $foo); + + for($i = 50; $i > 0; $i--) { + assertType('int<1, 50>', $i); + } + + assertType('int', $i); + } + + public function doCount(array $a) { + $foo = null; + for($i = 1; $i < count($a); $i++) { + $foo = new \stdClass(); + assertType('int<1, max>', $i); + } + + assertType('int<1, max>', $i); + assertType(\stdClass::class . '|null', $foo); + } + + public function doCount2() { + $foo = null; + for($i = 1; $i < count([]); $i++) { + $foo = new \stdClass(); + assertType('*NEVER*', $i); + } + + assertType('1', $i); + assertType('null', $foo); + } + + public function doBaz() { + for($i = 1; $i < 50; $i += 2) { + assertType('1|int<3, 49>', $i); + } + + assertType('int<50, max>', $i); + } + + public function doLOrem() { + for($i = 1; $i < 50; $i++) { + break; + } + + assertType('int<1, max>', $i); + } + +} + +interface Foo2 { + function equals(self $other): bool; +} + +class HelloWorld +{ + /** + * @param Foo2[] $startTimes + * @return mixed[] + */ + public static function groupCapacities(array $startTimes): array + { + if ($startTimes === []) { + return []; + } + sort($startTimes); + + $capacities = []; + $current = $startTimes[0]; + $count = 0; + foreach ($startTimes as $startTime) { + if (!$startTime->equals($current)) { + $count = 0; + } + $count++; + } + assertType('int<1, max>', $count); + + return $capacities; + } + + 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/nsrt/foreach-dependent-key-value.php b/tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php new file mode 100644 index 0000000000..a004c701b1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php @@ -0,0 +1,31 @@ + $val) { + assertType('int|string', $val); + if ($key === 'foo') { + assertType('int', $val); + } else { + assertType('string', $val); + } + + if ($key === 'bar') { + assertType('string', $val); + } else { + assertType('int', $val); + } + } + } + +} 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/nsrt/generic-class-string.php b/tests/PHPStan/Analyser/nsrt/generic-class-string.php new file mode 100644 index 0000000000..ed52a6fa21 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-class-string.php @@ -0,0 +1,162 @@ +|DateTimeInterface', $a); + assertType('DateTimeInterface', new $a()); + } else { + 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 { + assertType('mixed', $a); + } + + if (is_subclass_of($a, C::class)) { + assertType('int', $a::f()); + } else { + assertType('mixed', $a); + } +} + +/** + * @param object $a + */ +function testObject($a) { + assertType('object', new $a()); + + if (is_subclass_of($a, 'DateTimeInterface')) { + assertType('DateTimeInterface', $a); + } else { + assertType('object', $a); + } +} + +/** + * @param string $a + */ +function testString($a) { + assertType('object', new $a()); + + if (is_subclass_of($a, 'DateTimeInterface')) { + assertType('class-string', $a); + assertType('DateTimeInterface', new $a()); + } else { + assertType('string', $a); + } + + if (is_subclass_of($a, C::class)) { + assertType('int', $a::f()); + } else { + assertType('string', $a); + } +} + +/** + * @param string|object $a + */ +function testStringObject($a) { + assertType('object', new $a()); + + if (is_subclass_of($a, 'DateTimeInterface')) { + assertType('class-string|DateTimeInterface', $a); + assertType('DateTimeInterface', new $a()); + } else { + assertType('object|string', $a); + } + + if (is_subclass_of($a, C::class)) { + assertType('int', $a::f()); + } else { + assertType('object|string', $a); + } +} + +/** + * @param class-string<\DateTimeInterface> $a + */ +function testClassString($a) { + assertType('DateTimeInterface', new $a()); + + if (is_subclass_of($a, 'DateTime')) { + assertType('class-string', $a); + assertType('DateTime', new $a()); + } else { + assertType('class-string', $a); + } +} + +/** + * @param object|string $a + * @param class-string<\DateTimeInterface> $b + */ +function testClassStringAsClassName($a, string $b) { + assertType('object', new $a()); + + if (is_subclass_of($a, $b)) { + assertType('class-string|DateTimeInterface', $a); + assertType('DateTimeInterface', new $a()); + } else { + assertType('object|string', $a); + } + + if (is_subclass_of($a, $b, false)) { + assertType('DateTimeInterface', $a); + } else { + assertType('object|string', $a); + } +} + +function testClassExists(string $str) +{ + assertType('string', $str); + if (class_exists($str)) { + assertType('class-string', $str); + assertType('object', new $str()); + } + + $existentClass = \stdClass::class; + if (class_exists($existentClass)) { + assertType('\'stdClass\'', $existentClass); + } + + $nonexistentClass = 'NonexistentClass'; + if (class_exists($nonexistentClass)) { + assertType('\'NonexistentClass\'', $nonexistentClass); + } +} + +function testInterfaceExists(string $str) +{ + assertType('string', $str); + if (interface_exists($str)) { + assertType('class-string', $str); + } +} + +function testTraitExists(string $str) +{ + assertType('string', $str); + if (trait_exists($str)) { + assertType('class-string', $str); + } +} 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/nsrt/generic-generalization.php b/tests/PHPStan/Analyser/nsrt/generic-generalization.php new file mode 100644 index 0000000000..dcbde64d5b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-generalization.php @@ -0,0 +1,75 @@ + $genericClassString + * @param array{foo: 42} $arrayShape + * @param numeric-string $numericString + * @param non-empty-string $nonEmptyString + */ +function testUnbounded( + string $classString, + string $genericClassString, + string $string, + array $arrayShape, + string $numericString, + string $nonEmptyString +): void { + assertType('\'hello\'', unbounded('hello')); + assertType('\'stdClass\'', unbounded('stdClass')); + assertType('class-string', unbounded($classString)); + assertType('class-string', unbounded($genericClassString)); + + assertType("'hello'|class-string", unbounded(rand(0,1) === 1 ? 'hello' : $classString)); + + assertType('array{foo: 42}', unbounded($arrayShape)); + + assertType('numeric-string', unbounded($numericString)); + assertType('non-empty-string', unbounded($nonEmptyString)); +} + +/** + * @template T of string + * @param T $arg + * @return T + */ +function boundToString($arg) +{ + return $arg; +} + +/** + * @param class-string $classString + * @param class-string<\stdClass> $genericClassString + * @param non-empty-string $nonEmptyString + */ +function testBoundToString( + string $classString, + string $genericClassString, + string $nonEmptyString, + string $string +): void { + assertType('\'hello\'', boundToString('hello')); + assertType('\'stdClass\'', boundToString('stdClass')); + assertType('class-string', boundToString($classString)); + assertType('class-string', boundToString($genericClassString)); + + assertType('\'hello\'|class-string', boundToString(rand(0,1) === 1 ? 'hello' : $classString)); +} diff --git a/tests/PHPStan/Analyser/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/nsrt/generic-object-lower-bound.php b/tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php new file mode 100644 index 0000000000..fe71aab3d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php @@ -0,0 +1,75 @@ + $c + * @param T $d + * @return T + */ + function doFoo(Collection $c, object $d) + { + $c->add($d); + } + + /** + * @param Collection $c + */ + function doBar(Collection $c): void + { + assertType(Cat::class . '|' . Dog::class, $this->doFoo($c, new Cat())); + } + +} + +class Bar +{ + + /** + * @template T of object + * @param Collection2 $c + * @param T $d + * @return T + */ + function doFoo(Collection2 $c, object $d) + { + $c->add($d); + } + + /** + * @param Collection2 $c + */ + function doBar(Collection2 $c): void + { + assertType(Cat::class . '|' . Dog::class, $this->doFoo($c, new Cat())); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/generic-offset-get.php b/tests/PHPStan/Analyser/nsrt/generic-offset-get.php new file mode 100644 index 0000000000..8f2cfa4f5a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-offset-get.php @@ -0,0 +1,46 @@ + $offset + * @return T + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + + } + + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + + } + +} + +function (Foo $foo): void { + assertType(stdClass::class, $foo->offsetGet(stdClass::class)); + assertType(stdClass::class, $foo[stdClass::class]); +}; diff --git a/tests/PHPStan/Analyser/nsrt/generic-parent.php b/tests/PHPStan/Analyser/nsrt/generic-parent.php new file mode 100644 index 0000000000..0d41c95d0a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-parent.php @@ -0,0 +1,58 @@ + */ +class Bar extends Foo +{ + + public function doFoo() + { + assertType(Dog::class, parent::getAnimal()); + assertType(Dog::class, Foo::getAnimal()); + } + +} + +class E {} + +/** + * @template T of E + */ +class R { + + /** @return T */ + function ret() { return $this->e; } // nonsense, to silence missing return + + function test(): void { + assertType('T of GenericParent\E (class GenericParent\R, argument)', self::ret()); + assertType('T of GenericParent\E (class GenericParent\R, argument)', $this->ret()); + assertType('T of GenericParent\E (class GenericParent\R, argument)', static::ret()); + } +} 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/nsrt/generic-traits.php b/tests/PHPStan/Analyser/nsrt/generic-traits.php new file mode 100644 index 0000000000..c40d45c4d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-traits.php @@ -0,0 +1,226 @@ +doFoo(1)); + } + +} + +/** @template T of object */ +trait BarTrait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('object', $t); + } + +} + +/** @template T */ +class Bar +{ + + use BarTrait; + + public function doBar(): void + { + assertType('object', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar2Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('object', $t); + } + +} + +/** @template U */ +class Bar2 +{ + + use Bar2Trait; + + public function doBar(): void + { + assertType('object', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar3Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('stdClass', $t); + } + +} + +class Bar3 +{ + + /** @use Bar3Trait<\stdClass> */ + use Bar3Trait; + + public function doBar(): void + { + assertType('stdClass', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar4Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('U (class GenericTraits\Bar4, argument)', $t); + } + +} + +/** @template U */ +class Bar4 +{ + + /** @use Bar4Trait */ + use Bar4Trait; + + public function doBar(): void + { + assertType('U (class GenericTraits\Bar4, argument)', $this->doFoo()); + } + +} + +/** @template T of object */ +trait Bar5Trait +{ + + /** + * @param T $t + * @return T + */ + public function doFoo($t) + { + assertType('T (class GenericTraits\Bar5, argument)', $t); + } + +} + +/** @template T */ +class Bar5 +{ + + /** @use Bar5Trait */ + use Bar5Trait; + + public function doBar(): void + { + assertType('T (class GenericTraits\Bar5, argument)', $this->doFoo()); + } + + // sanity checks below (is T supposed to be an argument? yes) + + /** + * @param T $t + */ + public function doBaz($t) + { + assertType('T (class GenericTraits\Bar5, argument)', $t); + } + + /** + * @return T + */ + public function returnT() + { + + } + + public function doLorem() + { + assertType('T (class GenericTraits\Bar5, argument)', $this->returnT()); + } + +} + +/** @template T */ +trait Bar6Trait +{ + + /** @param T $t */ + public function doFoo($t) + { + assertType('int', $t); + } + +} + +/** @template U */ +trait Bar7Trait +{ + + /** @use Bar6Trait */ + use Bar6Trait; + +} + +class Bar7 +{ + + /** @use Bar7Trait */ + use Bar7Trait; + +} 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/nsrt/generics-default.php b/tests/PHPStan/Analyser/nsrt/generics-default.php new file mode 100644 index 0000000000..7bf2d3dff5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generics-default.php @@ -0,0 +1,44 @@ +doFoo()); + assertType('1', $this->doFoo(1)); + } + + /** + * @template T + * @param T $default + */ + public function doBaz($default = null): void + { + assertType('T (method GenericsDefault\Foo::doBaz(), argument)', $default); + } + + /** + * @template T + * @param T|null $default + */ + public function doLorem($default = null): void + { + assertType('T (method GenericsDefault\Foo::doLorem(), argument)|null', $default); + } + +} 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/nsrt/generics-empty-array.php b/tests/PHPStan/Analyser/nsrt/generics-empty-array.php new file mode 100644 index 0000000000..b238f3fdbf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generics-empty-array.php @@ -0,0 +1,80 @@ + $a + * @return array{TKey, T} + */ + public function doFoo(array $a = []): array + { + + } + + public function doBar() + { + assertType('array{*NEVER*, *NEVER*}', $this->doFoo()); + assertType('array{*NEVER*, *NEVER*}', $this->doFoo([])); + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection +{ + + /** + * @param array $items + */ + public function __construct(array $items = []) + { + + } + +} + +class Bar +{ + + public function doFoo() + { + assertType('GenericsEmptyArray\\ArrayCollection<*NEVER*, *NEVER*>', new ArrayCollection()); + assertType('GenericsEmptyArray\\ArrayCollection<*NEVER*, *NEVER*>', new ArrayCollection([])); + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection2 +{ + + public function __construct(array $items = []) + { + + } + +} + +class Baz +{ + + public function doFoo() + { + assertType('GenericsEmptyArray\\ArrayCollection2<(int|string), mixed>', new ArrayCollection2()); + assertType('GenericsEmptyArray\\ArrayCollection2<(int|string), mixed>', new ArrayCollection2([])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php b/tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php new file mode 100644 index 0000000000..4efa90e846 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php @@ -0,0 +1,35 @@ +', $this->doFoo($a)); + assertType('array', $this->doFoo($b)); + assertType('array', $this->doFoo($c)); + assertType('string', $this->doFoo($d)); + } + +} diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php similarity index 81% rename from tests/PHPStan/Analyser/data/generics.php rename to tests/PHPStan/Analyser/nsrt/generics.php index 8d95ffecb1..1968fab471 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -7,7 +7,7 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Generics\FunctionsAssertType\GenericRule; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; /** * @template T @@ -96,8 +96,8 @@ function testD($int, $float, $intFloat) assertType('float|int', d($int, $float)); assertType('DateTime|int', d($int, new \DateTime())); assertType('DateTime|float|int', d($intFloat, new \DateTime())); - assertType('array()|DateTime', d([], new \DateTime())); - assertType('array|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); + assertType('array{}|DateTime', d([], 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,18 +147,24 @@ function f($a, $b) */ function testF($arrayOfInt, $callableOrNull) { - assertType('array', f($arrayOfInt, 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 { return (string)$a; })); + assertType('Closure(mixed): string', function ($a): string { + return (string)$a; + }); 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, '')); } /** @@ -218,7 +224,7 @@ function testArrayMap(array $listOfIntegers) return (string) $int; }, $listOfIntegers); - assertType('array', $strings); + assertType('array', $strings); } /** @@ -341,7 +347,7 @@ function varAnnotation($cb) /** @var T */ $v = $cb(); - assertType('T (function PHPStan\Generics\FunctionsAssertType\varAnnotation(), argument)', $v); + assertType('T (function PHPStan\Generics\FunctionsAssertType\varAnnotation(), parameter)', $v); return $v; } @@ -365,7 +371,7 @@ public function f($p, $cb) /** @var T */ $v = $cb(); - assertType('T (class PHPStan\Generics\FunctionsAssertType\C, argument)', $v); + assertType('T (class PHPStan\Generics\FunctionsAssertType\C, parameter)', $v); // should be argument assertType('T (class PHPStan\Generics\FunctionsAssertType\C, argument)', $this->a); @@ -378,7 +384,7 @@ public function g() } }; - assertType('T (class PHPStan\Generics\FunctionsAssertType\C, argument)', $a->g()); + assertType('T (class PHPStan\Generics\FunctionsAssertType\C, parameter)', $a->g()); } } @@ -735,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())); @@ -757,7 +763,7 @@ function testClasses() $factory = new Factory(new \DateTime(), new A(1)); assertType( - 'array(DateTime, PHPStan\Generics\FunctionsAssertType\A, string, PHPStan\Generics\FunctionsAssertType\A)', + 'array{DateTime, PHPStan\\Generics\\FunctionsAssertType\\A, \'\', PHPStan\\Generics\\FunctionsAssertType\\A}', $factory->create(new \DateTime(), '', new A(new \DateTime())) ); } @@ -883,11 +889,11 @@ function cache1($t): void { function newHandling(): void { assertType('PHPStan\Generics\FunctionsAssertType\C', new C()); assertType('PHPStan\Generics\FunctionsAssertType\A', new A(new \stdClass())); - assertType('PHPStan\Generics\FunctionsAssertType\A<*ERROR*>', new A()); + assertType('PHPStan\Generics\FunctionsAssertType\A', new A()); } /** - * @template TKey + * @template TKey of array-key * @template TValue of \stdClass */ class StdClassCollection @@ -924,8 +930,8 @@ public function returnStatic(): self function () { $stdEmpty = new StdClassCollection([]); - assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection', $stdEmpty); - assertType('array', $stdEmpty->getAll()); + assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<*NEVER*, *NEVER*>', $stdEmpty); + assertType('array{}', $stdEmpty->getAll()); $std = new StdClassCollection([new \stdClass()]); assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection', $std); @@ -946,7 +952,7 @@ public function doFoo($a) /** @var T $b */ $b = doFoo(); - assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doFoo(), argument)', $b); + assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doFoo(), parameter)', $b); } /** @@ -959,7 +965,7 @@ public function doBar($a) /** @var T $b */ $b = doFoo(); - assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doBar(), argument)', $b); + assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doBar(), parameter)', $b); } } @@ -982,8 +988,8 @@ class CreateClassReflectionOfStaticClass public function doFoo() { assertType('PHPStan\Generics\FunctionsAssertType\CreateClassReflectionOfStaticClass', (new \ReflectionClass(self::class))->newInstanceWithoutConstructor()); - assertType('PHPStan\Generics\FunctionsAssertType\CreateClassReflectionOfStaticClass', (new \ReflectionClass(static::class))->newInstanceWithoutConstructor()); - assertType('class-string', (new \ReflectionClass(static::class))->name); + assertType('static(PHPStan\Generics\FunctionsAssertType\CreateClassReflectionOfStaticClass)', (new \ReflectionClass(static::class))->newInstanceWithoutConstructor()); + assertType('class-string', (new \ReflectionClass(static::class))->name); } } @@ -1028,8 +1034,8 @@ class StaticClassConstant public function doFoo() { $staticClassName = static::class; - assertType('class-string', $staticClassName); - assertType('static(PHPStan\Generics\FunctionsAssertType\StaticClassConstant)', new $staticClassName); + assertType('class-string)>', $staticClassName); + assertType('static(PHPStan\Generics\FunctionsAssertType\StaticClassConstant)', new $staticClassName); } /** @@ -1083,7 +1089,7 @@ function testGenericObjectWithoutClassType2($a) return $a; } - assertType('T of object (function PHPStan\Generics\FunctionsAssertType\testGenericObjectWithoutClassType2(), argument)', $b); + assertType('T of object~stdClass (function PHPStan\Generics\FunctionsAssertType\testGenericObjectWithoutClassType2(), argument)', $b); return $a; } @@ -1102,8 +1108,7 @@ function () { class GenericReflectionClass extends \ReflectionClass { - public $name; - + #[\ReturnTypeWillChange] public function newInstanceWithoutConstructor() { return parent::newInstanceWithoutConstructor(); @@ -1117,8 +1122,7 @@ public function newInstanceWithoutConstructor() class SpecificReflectionClass extends \ReflectionClass { - public $name; - + #[\ReturnTypeWillChange] public function newInstanceWithoutConstructor() { return parent::newInstanceWithoutConstructor(); @@ -1172,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 { @@ -1205,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); @@ -1218,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); }; /** @@ -1297,7 +1316,7 @@ function arrayOfGenericClassStrings(array $a): void function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject) { assertType( - 'class-string', + 'class-string|false', get_class($a) ); assertType( @@ -1322,7 +1341,7 @@ function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject) ); assertType('class-string', get_class($object)); - assertType('class-string', get_class($mixed)); + assertType('class-string|false', get_class($mixed)); assertType('class-string', get_class($tObject)); } @@ -1365,3 +1384,196 @@ public function method($one, $two): void assertType('float', $this->property); } } + +/** + * @template T + */ +interface GeneralFactoryInterface { + /** + * @return T + */ + public static function create(); +} + +class Car {} + +/** + * @implements GeneralFactoryInterface + */ +class CarFactory implements GeneralFactoryInterface { + public static function create() { return new Car(); } +} + +class CarFactoryProcessor { + /** + * @param class-string $class + */ + public function process($class): void { + $car = $class::create(); + assertType(Car::class, $car); + } +} + +function (\Throwable $e): void { + assertType('(int|string)', $e->getCode()); +}; + +function (): void { + $array = ['a' => 1, 'b' => 2]; + assertType('array{a: 1, b: 2}', a($array)); +}; + + +/** + * @template T of bool + * @param T $b + * @return T + */ +function boolBound(bool $b): bool +{ + return $b; +} + +function (bool $b): void { + assertType('true', boolBound(true)); + assertType('false', boolBound(false)); + assertType('bool', boolBound($b)); +}; + +/** + * @template T of float + * @param T $f + * @return T + */ +function floatBound(float $f): float +{ + return $f; +} + +function (float $f): void { + assertType('1.0', floatBound(1.0)); + assertType('float', floatBound($f)); +}; + +/** + * @template T of string|int|float|bool + */ +class UnionT +{ + + /** + * @param T|null $t + * @return T|null + */ + public function doFoo($t) + { + return $t; + } + +} + +/** + * @param UnionT $foo + */ +function foooo(UnionT $foo): void +{ + assertType('string|null', $foo->doFoo('a')); +} + +/** + * @template T1 of object + * @param T1 $type + * @return T1 + */ +function newObject($type): void +{ + assertType('T1 of object (function PHPStan\Generics\FunctionsAssertType\newObject(), argument)', new $type); +} + +function newStdClass(\stdClass $std): void +{ + assertType('stdClass', new $std); +} + +/** + * @template T1 of object + * @param class-string $type + * @return T1 + */ +function newClassString($type): void +{ + assertType('T1 of object (function PHPStan\Generics\FunctionsAssertType\newClassString(), argument)', new $type); +} + +/** + * @template T of array + * @param T $a + * @return T + */ +function arrayBound1(array $a): array +{ + return $a; +} + +/** + * @template T of array + * @param T $a + * @return T + */ +function arrayBound2(array $a): array +{ + return $a; +} + +/** + * @template T of list + * @param T $a + * @return T + */ +function arrayBound3(array $a): array +{ + return $a; +} + +/** + * @template T of list> + * @param T $a + * @return T + */ +function arrayBound4(array $a): array +{ + return $a; +} + +/** + * @template T of array + * @param T $a + * @return array + */ +function arrayBound5(array $a): array +{ + return $a; +} + +function (): void { + assertType('array{1: true}', arrayBound1([1 => true])); + assertType('array{\'a\', \'b\', \'c\'}', arrayBound2(range('a', 'c'))); + assertType('array', arrayBound2([1, 2, 3])); + assertType('array{true, false, true}', arrayBound3([true, false, true])); + assertType("array{array{a: 'a'}, array{b: 'b'}, array{c: 'c'}}", arrayBound4([['a' => 'a'], ['b' => 'b'], ['c' => 'c']])); + assertType('array', arrayBound5(range('a', 'c'))); +}; + +/** + * @template T of array{0: string, 1: bool} + * @param T $a + * @return T + */ +function constantArrayBound(array $a): array +{ + return $a; +} + +function (): void { + assertType('array{\'string\', true}', constantArrayBound(['string', true])); +}; diff --git a/tests/PHPStan/Analyser/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/nsrt/graphics-draw-return-types.php b/tests/PHPStan/Analyser/nsrt/graphics-draw-return-types.php new file mode 100644 index 0000000000..ab11c5d94f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/graphics-draw-return-types.php @@ -0,0 +1,11 @@ +>', $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..8018fd7f65 --- /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("'1'|'a'", $stringOrNull); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php b/tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php new file mode 100644 index 0000000000..fe3e49285d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/inc-dec-in-conditions.php @@ -0,0 +1,101 @@ +', $a); + } else { + assertType('int<0, max>', $a); + } + if (++$b <= 0) { + assertType('int', $b); + } else { + assertType('int<1, max>', $b); + } + if ($c++ < 0) { + assertType('int', $c); + } else { + assertType('int<1, max>', $c); + } + if ($d++ <= 0) { + assertType('int', $d); + } else { + assertType('int<2, max>', $d); + } +} + +function incRight(int $a, int $b, int $c, int $d): void +{ + if (0 < ++$a) { + assertType('int<1, max>', $a); + } else { + assertType('int', $a); + } + if (0 <= ++$b) { + assertType('int<0, max>', $b); + } else { + assertType('int', $b); + } + if (0 < $c++) { + assertType('int<2, max>', $c); + } else { + assertType('int', $c); + } + if (0 <= $d++) { + assertType('int<1, max>', $d); + } else { + assertType('int', $d); + } +} + +function decLeft(int $a, int $b, int $c, int $d): void +{ + if (--$a < 0) { + assertType('int', $a); + } else { + assertType('int<0, max>', $a); + } + if (--$b <= 0) { + assertType('int', $b); + } else { + assertType('int<1, max>', $b); + } + if ($c-- < 0) { + assertType('int', $c); + } else { + assertType('int<-1, max>', $c); + } + if ($d-- <= 0) { + assertType('int', $d); + } else { + assertType('int<0, max>', $d); + } +} + +function decRight(int $a, int $b, int $c, int $d): void +{ + if (0 < --$a) { + assertType('int<1, max>', $a); + } else { + assertType('int', $a); + } + if (0 <= --$b) { + assertType('int<0, max>', $b); + } else { + assertType('int', $b); + } + if (0 < $c--) { + assertType('int<0, max>', $c); + } else { + assertType('int', $c); + } + if (0 <= $d--) { + assertType('int<-1, max>', $d); + } else { + assertType('int', $d); + } +} 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 93% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-param.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-param.php index 2b2e0a03ba..16d5922e7c 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-param.php +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-param.php @@ -2,7 +2,7 @@ namespace InheritDocMergingParam; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class A {} class B extends A {} diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php similarity index 95% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php index b347f98cbc..f88b482748 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-return.php +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-return.php @@ -2,7 +2,7 @@ namespace InheritDocMergingReturn; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class A {} class B extends A {} diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php similarity index 87% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php index f9a638d018..087d2893af 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-template.php @@ -2,7 +2,7 @@ namespace InheritDocMergingTemplate; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { @@ -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 97% rename from tests/PHPStan/Analyser/data/inherit-phpdoc-merging-var.php rename to tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-var.php index 92676520c9..14f499a575 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-var.php +++ b/tests/PHPStan/Analyser/nsrt/inherit-phpdoc-merging-var.php @@ -2,7 +2,7 @@ namespace InheritDocMergingVar; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class A {} class B extends A {} diff --git a/tests/PHPStan/Analyser/data/inheritdoc-constructors.php b/tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php similarity index 89% rename from tests/PHPStan/Analyser/data/inheritdoc-constructors.php rename to tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php index bd5242c799..502379facd 100644 --- a/tests/PHPStan/Analyser/data/inheritdoc-constructors.php +++ b/tests/PHPStan/Analyser/nsrt/inheritdoc-constructors.php @@ -2,7 +2,7 @@ namespace InheritDocConstructors; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { diff --git a/tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php b/tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php similarity index 94% rename from tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php rename to tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php index 37fd8ea320..311bbaf064 100644 --- a/tests/PHPStan/Analyser/data/inheritdoc-parameter-remapping.php +++ b/tests/PHPStan/Analyser/nsrt/inheritdoc-parameter-remapping.php @@ -2,7 +2,7 @@ namespace InheritDocParameterRemapping; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Lorem { 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 @@ +', $class); + assertType(self::class, $foo); + if ($foo instanceof $class) { + assertType(self::class, $foo); + } else { + assertType(self::class, $foo); + } + } + +} + +class Bar extends Foo +{ + + public function doBar(Foo $foo, Bar $bar): void + { + $class = get_class($bar); + assertType('class-string', $class); + assertType(Foo::class, $foo); + if ($foo instanceof $class) { + assertType(self::class, $foo); + } else { + assertType('InstanceOfClassString\Foo', $foo); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/instanceof.php b/tests/PHPStan/Analyser/nsrt/instanceof.php similarity index 87% rename from tests/PHPStan/Analyser/data/instanceof.php rename to tests/PHPStan/Analyser/nsrt/instanceof.php index 5e291cfefd..fe111fb0e4 100644 --- a/tests/PHPStan/Analyser/data/instanceof.php +++ b/tests/PHPStan/Analyser/nsrt/instanceof.php @@ -4,7 +4,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; interface BarInterface { @@ -19,11 +19,10 @@ abstract class BarParent class Foo extends BarParent { - public function someMethod(Expr $foo) + public function someMethod(Expr $foo, Foo $intersected) { $bar = $foo; $baz = doFoo(); - $intersected = new Foo(); $parent = doFoo(); if ($baz instanceof Foo) { @@ -80,9 +79,9 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('true', $subject instanceof Foo); assertType('bool', $subject instanceof $classString); } else { - assertType('mixed~InstanceOfNamespace\Foo', $subject); - assertType('false', $subject instanceof Foo); - assertType('false', $subject instanceof $classString); + assertType('mixed', $subject); + assertType('bool', $subject instanceof Foo); + assertType('bool', $subject instanceof $classString); // could be false } $constantString = 'InstanceOfNamespace\BarParent'; @@ -132,24 +131,24 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); assertType('bool', $subject instanceof $objectT); } else { - assertType('mixed~ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); - assertType('false', $subject instanceof $objectT); + assertType('mixed', $subject); + assertType('bool', $subject instanceof $objectT); // can be false } if ($subject instanceof $objectTString) { assertType('ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); assertType('bool', $subject instanceof $objectTString); } else { - assertType('mixed~ObjectT of InstanceOfNamespace\BarInterface (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); - assertType('false', $subject instanceof $objectTString); + assertType('mixed', $subject); + assertType('bool', $subject instanceof $objectTString); // can be false } if ($subject instanceof $mixedTString) { assertType('MixedT (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)&object', $subject); assertType('bool', $subject instanceof $mixedTString); } else { - assertType('mixed~MixedT (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); - assertType('false', $subject instanceof $mixedTString); + assertType('mixed', $subject); + assertType('bool', $subject instanceof $mixedTString); // can be false } if ($subject instanceof $string) { @@ -180,8 +179,8 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('InstanceOfNamespace\Foo', $object); assertType('bool', $object instanceof $classString); } else { - assertType('object~InstanceOfNamespace\Foo', $object); - assertType('false', $object instanceof $classString); + assertType('object', $object); + assertType('bool', $object instanceof $classString); // could be false } if ($instance instanceof $string) { diff --git a/tests/PHPStan/Analyser/nsrt/int-mask.php b/tests/PHPStan/Analyser/nsrt/int-mask.php new file mode 100644 index 0000000000..797d24f8d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/int-mask.php @@ -0,0 +1,43 @@ + $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/nsrt/intersection-static.php b/tests/PHPStan/Analyser/nsrt/intersection-static.php new file mode 100644 index 0000000000..bfcc8f731a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/intersection-static.php @@ -0,0 +1,71 @@ +returnStatic()); + } + + /** + * @param Foo&Baz $intersection + */ + public function doBar($intersection) + { + assertType('IntersectionStatic\Baz&IntersectionStatic\Foo', $intersection); + assertType('IntersectionStatic\Baz&IntersectionStatic\Foo', $intersection->returnStatic()); + } + +} + +abstract class Ipsum implements Foo +{ + + public function testThis(): void + { + assertType('static(IntersectionStatic\Ipsum)', $this->returnStatic()); + if ($this instanceof Bar) { + assertType('$this(IntersectionStatic\Ipsum)&IntersectionStatic\Bar', $this); + assertType('$this(IntersectionStatic\Ipsum)&IntersectionStatic\Bar', $this->returnStatic()); + } + if ($this instanceof Baz) { + assertType('$this(IntersectionStatic\Ipsum)&IntersectionStatic\Baz', $this); + assertType('$this(IntersectionStatic\Ipsum)&IntersectionStatic\Baz', $this->returnStatic()); + } + } + +} 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/nsrt/invalidate-object-argument-function.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php new file mode 100644 index 0000000000..abd0449da4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php @@ -0,0 +1,78 @@ +getName()); + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + doBar($foo); + assertType('\'foo\'', $foo->getName()); + assertType(Foo::class, $foo); + + doBaz($foo); + assertType('\'foo\'', $foo->getName()); + assertType(Foo::class, $foo); + + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + doLorem($foo); + assertType('string', $foo->getName()); + assertType(Foo::class, $foo); + + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + doIpsum($foo); + assertType('string', $foo->getName()); + assertType(Foo::class, $foo); + } + +} + +/** + * @phpstan-pure + */ +function doBar($arg) +{ + +} + +function doBaz($arg) +{ + +} + +function doLorem($arg): void +{ + +} + +/** @phpstan-impure */ +function doIpsum($arg) +{ + +} diff --git a/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php new file mode 100644 index 0000000000..0a015983cb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php @@ -0,0 +1,78 @@ +getName()); + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + self::doBar($foo); + assertType('\'foo\'', $foo->getName()); + assertType(Foo::class, $foo); + + self::doBaz($foo); + assertType('\'foo\'', $foo->getName()); + assertType(Foo::class, $foo); + + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + self::doLorem($foo); + assertType('string', $foo->getName()); + assertType(Foo::class, $foo); + + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + self::doIpsum($foo); + assertType('string', $foo->getName()); + assertType(Foo::class, $foo); + } + + /** + * @phpstan-pure + */ + public static function doBar($arg) + { + + } + + public static function doBaz($arg) + { + + } + + public static function doLorem($arg): void + { + + } + + /** @phpstan-impure */ + public static function doIpsum($arg) + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php new file mode 100644 index 0000000000..806f1e0d2c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php @@ -0,0 +1,78 @@ +getName()); + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + $this->doBar($foo); + assertType('\'foo\'', $foo->getName()); + assertType(Foo::class, $foo); + + $this->doBaz($foo); + assertType('\'foo\'', $foo->getName()); + assertType(Foo::class, $foo); + + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + $this->doLorem($foo); + assertType('string', $foo->getName()); + assertType(Foo::class, $foo); + + assert($foo->getName() === 'foo'); + assertType('\'foo\'', $foo->getName()); + + $this->doIpsum($foo); + assertType('string', $foo->getName()); + assertType(Foo::class, $foo); + } + + /** + * @phpstan-pure + */ + public function doBar($arg) + { + + } + + public function doBaz($arg) + { + + } + + public function doLorem($arg): void + { + + } + + /** @phpstan-impure */ + public function doIpsum($arg) + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php b/tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php new file mode 100644 index 0000000000..5eb178a8a8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php @@ -0,0 +1,31 @@ += 8.1 + +namespace InvalidateReadonlyProperties; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + } + + public function doFoo(): void + { + if ($this->foo === 1) { + assertType('1', $this->foo); + $this->doBar(); + assertType('1', $this->foo); + } + } + + public function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/is-a.php b/tests/PHPStan/Analyser/nsrt/is-a.php new file mode 100644 index 0000000000..8db2aada66 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/is-a.php @@ -0,0 +1,82 @@ + $fooClassString */ + $fooClassString = 'Foo'; + + if (is_a($foo, $fooClassString)) { + \PHPStan\Testing\assertType('IsA\Foo', $foo); + } +}; + +function (string $foo) { + if (is_a($foo, Foo::class, true)) { + \PHPStan\Testing\assertType('class-string', $foo); + } +}; + +function (string $foo, string $someString) { + if (is_a($foo, $someString, true)) { + \PHPStan\Testing\assertType('class-string', $foo); + } +}; + +function (Bar $a, Bar $b, Bar $c, Bar $d) { + if (is_a($a, Bar::class)) { + \PHPStan\Testing\assertType('IsA\Bar', $a); + } + + if (is_a($b, Foo::class)) { + \PHPStan\Testing\assertType('IsA\Bar', $b); + } + + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_a($c, $barClassString)) { + \PHPStan\Testing\assertType('IsA\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_a($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsA\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_a($a, Bar::class, true)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_a($b, Foo::class, true)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_a($c, $barClassString, true)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_a($d, $fooClassString, true)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Analyser/nsrt/is-numeric.php b/tests/PHPStan/Analyser/nsrt/is-numeric.php new file mode 100644 index 0000000000..65e339dec1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/is-numeric.php @@ -0,0 +1,9 @@ + $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_subclass_of($a, Bar::class)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_subclass_of($b, Foo::class)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Analyser/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/nsrt/isset-coalesce-empty-type-root.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-root.php new file mode 100644 index 0000000000..2d027da378 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-root.php @@ -0,0 +1,19 @@ +string)); + } + + function coalesce() + { + + $scalar = 3; + + assertType('true', isset($scalar)); + + $array = [1, 2, 3]; + + assertType('false', isset($array['string'])); + + $multiDimArray = [[1], [2], [3]]; + + assertType('false', isset($multiDimArray['string'])); + + assertType('false', isset($doesNotExist)); + + if (rand() > 0.5) { + $maybeVariable = 3; + } + + assertType('bool', isset($maybeVariable)); + + $fixedDimArray = [ + 'dim' => 1, + 'dim-null' => rand() > 0.5 ? null : 1, + 'dim-null-offset' => ['a' => rand() > 0.5 ? true : null], + 'dim-empty' => [] + ]; + + // Always set + assertType('true', isset($fixedDimArray['dim'])); + + // Maybe set + assertType('bool', isset($fixedDimArray['dim-null'])); + + // Never set, then unknown + assertType('false', isset($fixedDimArray['dim-null-not-set']['a'])); + + // Always set, then always set + assertType('bool', isset($fixedDimArray['dim-null-offset']['a'])); + + // Always set, then never set + assertType('false', isset($fixedDimArray['dim-empty']['b'])); + + $foo = new FooIsset(); + + assertType('bool', isset($foo->stringOrNull)); + + assertType('true', isset($foo->string)); + + assertType('false', isset($foo->alwaysNull)); + + assertType('true', isset($foo->FooIsset->string)); + + assertType('bool', isset($foo->FooIssetOrNull->string)); + + assertType('bool', isset(FooIsset::$staticStringOrNull)); + + assertType('true', isset(FooIsset::$staticString)); + + assertType('false', isset(FooIsset::$staticAlwaysNull)); + } + + /** + * @param array $array + */ + function coalesceStringOffset(array $array) + { + assertType('bool', isset($array['string'])); + } + + function alwaysNullCoalesce (?string $a): void + { + if (!is_string($a)) { + assertType('false', isset($a)); + } + } + + function fooo(): void { + assertType('true', isset((new FooIsset())->string)); + assertType('bool', isset((new FooIsset())->stringOrNull)); + assertType('false', isset((new FooIsset())->alwaysNull)); + } + + function fooooo(FooIsset $foo): void + { + assertType('false', isset($foo::$staticAlwaysNull)); + assertType('true', isset($foo::$staticString)); + assertType('bool', isset($foo::$staticStringOrNull)); + } +} + +/** + * @property int $integerProperty + * @property FooIsset $foo + */ +class SomeMagicProperties +{ + + function doFoo(SomeMagicProperties $foo, \stdClass $std): void { + assertType('bool', isset($foo->integerProperty)); + + assertType('bool', isset($foo->foo->string)); + + assertType('bool', isset($std->foo)); + } + + function numericStringOffset(string $code): string + { + $array = [1, 2, 3]; + assertType('bool', isset($array[$code])); + + if (isset($array[$code])) { + return (string) $array[$code]; + } + + $mappings = [ + '21021200' => '21028800', + ]; + + assertType('bool', isset($mappings[$code])); + + if (isset($mappings[$code])) { + return (string) $mappings[$code]; + } + + throw new \RuntimeException(); + } + + + /** + * @param array{foo: string} $array + * @param 'bar' $bar + */ + function offsetFromPhpdoc(array $array, string $bar) + { + assertType('true', isset($array['foo'])); + + $array = ['bar' => 1]; + assertType('true', isset($array[$bar])); + } + + +} + +class FooNativeProp +{ + + public int $hasDefaultValue = 0; + + public int $isAssignedBefore; + + public int $canBeUninitialized; + + function doFoo(FooNativeProp $foo): void { + assertType('bool', isset($foo->hasDefaultValue)); + + $foo->isAssignedBefore = 5; + assertType('true', isset($foo->isAssignedBefore)); + + assertType('bool', isset($foo->canBeUninitialized)); + } + +} + +class Bug4290Isset +{ + public function test(): void + { + $array = self::getArray(); + + assertType('bool', isset($array['status'])); + assertType('bool', isset($array['value'])); + + $data = array_filter([ + 'status' => isset($array['status']) ? $array['status'] : null, + 'value' => isset($array['value']) ? $array['value'] : null, + ]); + + if (count($data) === 0) { + return; + } + + assertType('bool', isset($data['status'])); + + isset($data['status']) ? 1 : 0; + } + + /** + * @return string[] + */ + public static function getArray(): array + { + return ['value' => '100']; + } +} + +class Bug4671 +{ + + /** + * @param array $strings + */ + public function doFoo(int $intput, array $strings): void + { + assertType('bool', isset($strings[(string) $intput])); + } + +} + +class MoreIsset +{ + + function one() + { + + /** @var string|null $alwaysDefinedNullable */ + $alwaysDefinedNullable = doFoo(); + + assertType('bool', isset($alwaysDefinedNullable)); + + $alwaysDefinedNotNullable = 'string'; + assertType('true', isset($alwaysDefinedNotNullable)); + + if (doFoo()) { + $sometimesDefinedVariable = 1; + } + + assertType('bool', isset( + $sometimesDefinedVariable // fine, this is what's isset() is for + )); + + assertType('false', isset( + $sometimesDefinedVariable, // fine, this is what's isset() is for + $neverDefinedVariable // always false + )); + + assertType('false', isset( + $neverDefinedVariable // always false + )); + + /** @var array|null $anotherAlwaysDefinedNullable */ + $anotherAlwaysDefinedNullable = doFoo(); + + assertType('bool', isset($anotherAlwaysDefinedNullable['test']['test'])); + + /** @var array $anotherAlwaysDefinedNotNullable */ + $anotherAlwaysDefinedNotNullable = doFoo(); + assertType('bool', isset($anotherAlwaysDefinedNotNullable['test']['test'])); + + assertType('false', isset($anotherNeverDefinedVariable['test']['test']->test['test']['test'])); + + assertType('false', isset($yetAnotherNeverDefinedVariable::$test['test'])); + + assertType('bool', isset($_COOKIE['test'])); + + assertType('false', isset($yetYetAnotherNeverDefinedVariableInIsset)); + + if (doFoo()) { + $yetAnotherVariableThatSometimesExists = 1; + } + + assertType('bool', isset($yetAnotherVariableThatSometimesExists)); + + /** @var string|null $nullableVariableUsedInTernary */ + $nullableVariableUsedInTernary = doFoo(); + assertType('bool', isset($nullableVariableUsedInTernary)); + } + + function two() { + $alwaysDefinedNotNullable = 'string'; + if (doFoo()) { + $sometimesDefinedVariable = 1; + } + + assertType('false', isset( + $alwaysDefinedNotNullable, // always true + $sometimesDefinedVariable, // fine, this is what's isset() is for + $neverDefinedVariable // always false + )); + + assertType('true', isset( + $alwaysDefinedNotNullable // always true + )); + + assertType('bool', isset( + $alwaysDefinedNotNullable, // always true + $sometimesDefinedVariable // fine, this is what's isset() is for + )); + } + + function three() { + $null = null; + + assertType('false', isset($null)); + } + + function four() { + assertType('bool', isset($_SESSION)); + assertType('bool', isset($_SESSION['foo'])); + } + +} + +class FooCoalesce +{ + /** @var string|null */ + public static $staticStringOrNull = null; + + /** @var string */ + public static $staticString = ''; + + /** @var null */ + public static $staticAlwaysNull; + + /** @var string|null */ + public $stringOrNull = null; + + /** @var string */ + public $string = ''; + + /** @var null */ + public $alwaysNull; + + /** @var FooCoalesce|null */ + public $fooCoalesceOrNull; + + /** @var FooCoalesce */ + public $fooCoalesce; + + public function thisCoalesce() { + assertType('string', $this->string ?? false); + } + + function coalesce() + { + + $scalar = 3; + + assertType('3', $scalar ?? 4); + + $array = [1, 2, 3]; + + assertType('0', $array['string'] ?? 0); + + $multiDimArray = [[1], [2], [3]]; + + assertType('0', $multiDimArray['string'] ?? 0); + + assertType('0', $doesNotExist ?? 0); + + if (rand() > 0.5) { + $maybeVariable = 3; + } + + assertType('0|3', $maybeVariable ?? 0); + + $fixedDimArray = [ + 'dim' => 1, + 'dim-null' => rand() > 0.5 ? null : 1, + 'dim-null-offset' => ['a' => rand() > 0.5 ? true : null], + 'dim-empty' => [] + ]; + + // Always set + assertType('1', $fixedDimArray['dim'] ?? 0); + + // Maybe set + assertType('0|1', $fixedDimArray['dim-null'] ?? 0); + + // Never set, then unknown + assertType('0', $fixedDimArray['dim-null-not-set']['a'] ?? 0); + + // Always set, then always set + assertType('0|true', $fixedDimArray['dim-null-offset']['a'] ?? 0); + + // Always set, then never set + assertType('0', $fixedDimArray['dim-empty']['b'] ?? 0); + + assertType('int<0, max>', rand() ?? false); + + assertType('0|(lowercase-string&uppercase-string)', preg_replace('', '', '') ?? 0); + + $foo = new FooCoalesce(); + + assertType('string|false', $foo->stringOrNull ?? false); + + assertType('string', $foo->string ?? false); + + assertType('\'\'', $foo->alwaysNull ?? ''); + + assertType('string', $foo->fooCoalesce->string ?? false); + + assertType('string|false', $foo->fooCoalesceOrNull->string ?? false); + + assertType('string|false', FooCoalesce::$staticStringOrNull ?? false); + + assertType('string', FooCoalesce::$staticString ?? false); + + assertType('false', FooCoalesce::$staticAlwaysNull ?? false); + } + + /** + * @param array $array + */ + function coalesceStringOffset(array $array) + { + assertType('int|false', $array['string'] ?? false); + } + + function alwaysNullCoalesce (?string $a): void + { + if (!is_string($a)) { + assertType('false', $a ?? false); + } + } + + function foo(): void { + assertType('string', (new FooCoalesce())->string ?? false); + assertType('string|false', (new FooCoalesce())->stringOrNull ?? false); + assertType('false', (new FooCoalesce())->alwaysNull ?? false); + + assertType(FooCoalesce::class, (new FooCoalesce()) ?? false); + assertType('\'foo\'', null ?? 'foo'); + } + + function bar(FooCoalesce $foo): void + { + assertType('false', $foo::$staticAlwaysNull ?? false); + assertType('string', $foo::$staticString ?? false); + assertType('string|false', $foo::$staticStringOrNull ?? false); + } + + function lorem(): void { + assertType('\'foo\'', $foo ?? 'foo'); + assertType('\'foo\'', $bar->bar ?? 'foo'); + } + + function ipsum(): void { + $scalar = 3; + assertType('3', $scalar ?? 4); + assertType('0', $doesNotExist ?? 0); + } + + function ipsum2(?string $a): void { + if (!is_string($a)) { + assertType('\'foo\'', $a ?? 'foo'); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/iterator-iterator.php b/tests/PHPStan/Analyser/nsrt/iterator-iterator.php new file mode 100644 index 0000000000..e17aae1d25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/iterator-iterator.php @@ -0,0 +1,20 @@ + $it + */ + public function doFoo(\ArrayIterator $it): void + { + $iteratorIterator = new \IteratorIterator($it); + assertType('Iterator', $iteratorIterator->getInnerIterator()); + assertType('array', $iteratorIterator->getArrayCopy()); + } + +} 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/nsrt/key-of.php b/tests/PHPStan/Analyser/nsrt/key-of.php new file mode 100644 index 0000000000..f52eb2effd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/key-of.php @@ -0,0 +1,41 @@ + 'John F. Kennedy Airport', + self::LGA => 'La Guardia Airport', + ]; + + /** + * @param key-of $code + */ + public static function foo(string $code): void + { + assertType('\'jfk\'|\'lga\'', $code); + } + + /** + * @param key-of<'jfk'> $code + */ + public static function bar(string $code): void + { + assertType('string', $code); + } + + /** + * @param key-of<'jfk'|'lga'> $code + */ + public static function baz(string $code): void + { + assertType('string', $code); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/math.php b/tests/PHPStan/Analyser/nsrt/math.php new file mode 100644 index 0000000000..9d12809783 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/math.php @@ -0,0 +1,178 @@ +', self::MAX_TOTAL_PRODUCTS - count($excluded)); + assertType('int', self::MAX_TOTAL_PRODUCTS - $i); + + $maxOrPlusOne = self::MAX_TOTAL_PRODUCTS; + if (rand(0, 1)) { + $maxOrPlusOne++; + } + + assertType('22|23', $maxOrPlusOne); + assertType('int', $maxOrPlusOne - count($excluded)); + } + + public function doBar(int $notZero): void + { + if ($notZero === 0) { + return; + } + + assertType('int|int<2, max>', $notZero + 1); + } + + /** + * @param int<-5, 5> $rangeFiveBoth + * @param int<-5, max> $rangeFiveLeft + * @param int $rangeFiveRight + */ + public function doBaz(int $rangeFiveBoth, int $rangeFiveLeft, int $rangeFiveRight): void + { + assertType('int<-4, 6>', $rangeFiveBoth + 1); + assertType('int<-4, max>', $rangeFiveLeft + 1); + assertType('int<-6, max>', $rangeFiveLeft - 1); + assertType('int', $rangeFiveRight + 1); + assertType('int', $rangeFiveRight - 1); + + assertType('int', $rangeFiveLeft + $rangeFiveRight); + assertType('int', $rangeFiveLeft - $rangeFiveRight); + + assertType('int', $rangeFiveRight + $rangeFiveLeft); + assertType('int', $rangeFiveRight - $rangeFiveLeft); + + assertType('int<-10, 10>', $rangeFiveBoth + $rangeFiveBoth); + assertType('int<-10, 10>', $rangeFiveBoth - $rangeFiveBoth); + + assertType('int<-10, max>', $rangeFiveBoth + $rangeFiveLeft); + assertType('int', $rangeFiveBoth - $rangeFiveLeft); + + assertType('int', $rangeFiveBoth + $rangeFiveRight); + assertType('int<-10, max>', $rangeFiveBoth - $rangeFiveRight); + + assertType('int<-10, max>', $rangeFiveLeft + $rangeFiveBoth); + assertType('int<-10, max>', $rangeFiveLeft - $rangeFiveBoth); + + assertType('int', $rangeFiveRight + $rangeFiveBoth); + assertType('int', $rangeFiveRight - $rangeFiveBoth); + } + + public function doLorem($a, $b): void + { + $nullsReverse = rand(0, 1) ? 1 : -1; + $comparison = $a <=> $b; + assertType('int<-1, 1>', $comparison); + assertType('-1|1', $nullsReverse); + assertType('int<-1, 1>', $comparison * $nullsReverse); + } + + public function doIpsum(int $newLevel): void + { + $min = min(30, $newLevel); + assertType('int', $min); + $minDivFive = $min / 5; + assertType('float|int', $minDivFive); + $volume = 0x10000000 * $minDivFive; + assertType('float|int', $volume); + } + + public function doDolor(int $i): void + { + $chunks = min(200, $i); + assertType('int', $chunks); + $divThirty = $chunks / 30; + assertType('float|int', $divThirty); + assertType('float|int', $divThirty + 3); + } + + public function doSit(int $i, int $j): void + { + if ($i < 0) { + return; + } + if ($j < 1) { + return; + } + + assertType('int<0, max>', $i); + assertType('int<1, max>', $j); + assertType('int', $i - $j); + } + + /** + * @param int<-5, 5> $range + */ + public function multiplyZero(int $i, float $f, $range): void + { + assertType('0', $i * false); + assertType('0.0', $f * false); + assertType('0', $range * false); + + assertType('0', $i * '0'); + assertType('0.0', $f * '0'); + assertType('0', $range * '0'); + + assertType('0', $i * 0); + assertType('0.0', $f * 0); + assertType('0', $range * 0); + + assertType('0', 0 * $i); + assertType('0.0', 0 * $f); + assertType('0', 0 * $range); + + $i *= 0; + $f *= 0; + $range *= 0; + assertType('0', $i); + assertType('0.0', $f); + assertType('0', $range); + + } + + 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/nsrt/mb_substitute_character-php8.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php new file mode 100644 index 0000000000..2933d4ccab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php @@ -0,0 +1,21 @@ += 8.0 + +\PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<0, 55295>|int<57344, 1114111>', mb_substitute_character()); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('')); +\PHPStan\Testing\assertType('true', mb_substitute_character(null)); +\PHPStan\Testing\assertType('true', mb_substitute_character('none')); +\PHPStan\Testing\assertType('true', mb_substitute_character('long')); +\PHPStan\Testing\assertType('true', mb_substitute_character('entity')); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('foo')); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('123')); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('123.4')); +\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD)); +\PHPStan\Testing\assertType('true', mb_substitute_character(0x10FFFF)); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(-1)); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(0x110000)); +\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined)); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(new stdClass())); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(function () {})); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(rand(0xD800, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF))); diff --git a/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php new file mode 100644 index 0000000000..41a921c9f7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php @@ -0,0 +1,21 @@ +|int<57344, 1114111>', mb_substitute_character()); +\PHPStan\Testing\assertType('true', mb_substitute_character('')); +\PHPStan\Testing\assertType('false', mb_substitute_character(null)); +\PHPStan\Testing\assertType('true', mb_substitute_character('none')); +\PHPStan\Testing\assertType('true', mb_substitute_character('long')); +\PHPStan\Testing\assertType('true', mb_substitute_character('entity')); +\PHPStan\Testing\assertType('false', mb_substitute_character('foo')); +\PHPStan\Testing\assertType('true', mb_substitute_character('123')); +\PHPStan\Testing\assertType('true', mb_substitute_character('123.4')); +\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD)); +\PHPStan\Testing\assertType('true', mb_substitute_character(0x10FFFF)); +\PHPStan\Testing\assertType('false', mb_substitute_character(-1)); +\PHPStan\Testing\assertType('false', mb_substitute_character(0x110000)); +\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined)); +\PHPStan\Testing\assertType('bool', mb_substitute_character(new stdClass())); +\PHPStan\Testing\assertType('bool', mb_substitute_character(function () {})); +\PHPStan\Testing\assertType('false', mb_substitute_character(rand(0xD800, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF))); diff --git a/tests/PHPStan/Analyser/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/nsrt/missing-closure-native-return-typehint.php b/tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php new file mode 100644 index 0000000000..d516f89f23 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php @@ -0,0 +1,55 @@ +', (function (bool $bool) { + if ($bool) { + return; + } else { + yield 1; + } + })()); + \PHPStan\Testing\assertType('1|null', (function (bool $bool) { + if ($bool) { + return; + } else { + return 1; + } + })()); + \PHPStan\Testing\assertType('1', (function (): int { + return 1; + })()); + \PHPStan\Testing\assertType('1|null', (function (bool $bool) { + if ($bool) { + return null; + } else { + return 1; + } + })()); + \PHPStan\Testing\assertType('1', (function (bool $bool) { + if ($bool) { + return 1; + } + })()); + + \PHPStan\Testing\assertType('array{foo: \'bar\'}', (function () { + $array = [ + 'foo' => 'bar', + ]; + + return $array; + })()); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/mixed-typehint.php b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php new file mode 100644 index 0000000000..5b3c17cbb1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php @@ -0,0 +1,39 @@ += 8.0 + +namespace MixedTypehint; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(mixed $foo) + { + assertType('mixed', $foo); + assertType('mixed', $this->doBar()); + } + + public function doBar(): mixed + { + + } + +} + +function doFoo(mixed $foo) +{ + assertType('mixed', $foo); +} + +function (mixed $foo) { + assertType('mixed', $foo); + $f = function (): mixed { + + }; + assertType('void', $f()); + + $f = function () use ($foo): mixed { + return $foo; + }; + assertType('mixed', $f()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/model-mixin.php b/tests/PHPStan/Analyser/nsrt/model-mixin.php new file mode 100644 index 0000000000..e6598d2a15 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/model-mixin.php @@ -0,0 +1,50 @@ += 8.0 + +namespace ModelMixin; + +use function PHPStan\Testing\assertType; + +/** @mixin Builder */ +class Model +{ + /** @param array $args */ + public static function __callStatic(string $method, array $args): mixed + { + (new self)->$method(...$args); + } +} + +/** @template TModel as Model */ +class Builder +{ + /** @return array */ + public function all() { return []; } +} + +class User extends Model +{ +} + +function (): void { + assertType('array', User::all()); +}; + +class MixedMethod +{ + + public function doFoo(): int + { + return 1; + } + +} + +/** @mixin MixedMethod */ +interface InterfaceWithMixin +{ + +} + +function (InterfaceWithMixin $i): void { + assertType('int', $i->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/nsrt/modulo-operator.php b/tests/PHPStan/Analyser/nsrt/modulo-operator.php new file mode 100644 index 0000000000..a4876e59f1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/modulo-operator.php @@ -0,0 +1,76 @@ + $range + * @param int<0, max> $zeroOrMore + * @param 1|2|3 $intConst + * @param int|int<4, max> $unionRange + * @param int|7 $hybridUnionRange + */ + function doBar(int $i, int $j, $p, $range, $zeroOrMore, $intConst, $unionRange, $hybridUnionRange, $mixed) + { + assertType('int<-1, 1>', $i % 2); + assertType('int<0, 1>', $p % 2); + + assertType('int<-2, 2>', $i % 3); + assertType('int<0, 2>', $p % 3); + + assertType('0|1|2', $intConst % 3); + assertType('int<-2, 2>', $i % $intConst); + assertType('int<0, 2>', $p % $intConst); + + assertType('int<0, 2>', $range % 3); + + assertType('int<-9, 9>', $i % $range); + assertType('int<0, 9>', $p % $range); + + assertType('int', $i % $unionRange); + assertType('int<0, max>', $p % $unionRange); + + assertType('int<-6, 6>', $i % $hybridUnionRange); + assertType('int<0, 6>', $p % $hybridUnionRange); + + assertType('int<0, max>', $zeroOrMore % $mixed); + + if ($i === 0) { + return; + } + + assertType('int', $j % $i); + } + + function moduleOne(int $i, float $f) { + assertType('0', true % '1'); + assertType('0', false % '1'); + assertType('0', null % '1'); + assertType('0', -1 % '1'); + assertType('0', 0 % '1'); + assertType('0', 1 % '1'); + assertType('0', '1' % '1'); + assertType('0', 1.24 % '1'); + + assertType('0', $i % 1.0); + assertType('0', $f % 1.0); + + assertType('0', $i % '1.0'); + assertType('0', $f % '1.0'); + + assertType('0', $i % '1'); + assertType('0', $f % '1'); + + assertType('0', $i % true); + assertType('0', $f % true); + + $i %= '1'; + $f %= '1'; + assertType('0', $i); + assertType('0', $f); + } +} diff --git a/tests/PHPStan/Analyser/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/nsrt/more-type-strings.php b/tests/PHPStan/Analyser/nsrt/more-type-strings.php new file mode 100644 index 0000000000..0951303e20 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/more-type-strings.php @@ -0,0 +1,29 @@ + $genericInterfaceString + * @param trait-string $genericTraitString + */ + public function doFoo( + string $interfaceString, + string $traitString, + string $genericInterfaceString, + string $genericTraitString + ): void + { + assertType('class-string', $interfaceString); + assertType('class-string', $traitString); + assertType('class-string', $genericInterfaceString); + assertType('string', $genericTraitString); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/multi-assign.php b/tests/PHPStan/Analyser/nsrt/multi-assign.php new file mode 100644 index 0000000000..ab4e4de1cf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/multi-assign.php @@ -0,0 +1,83 @@ +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/nsrt/native-intersection.php b/tests/PHPStan/Analyser/nsrt/native-intersection.php new file mode 100644 index 0000000000..c7dcb7c71c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/native-intersection.php @@ -0,0 +1,29 @@ += 8.1 + +namespace NativeIntersection; + +use function PHPStan\Testing\assertType; + +interface A +{ + +} + +interface B +{ + +} + +class Foo +{ + + private A&B $prop; + + public function doFoo(A&B $ab): A&B + { + assertType('NativeIntersection\A&NativeIntersection\B', $this->prop); + assertType('NativeIntersection\A&NativeIntersection\B', $ab); + assertType('NativeIntersection\A&NativeIntersection\B', $this->doFoo($ab)); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/nested-generic-incomplete-constructor.php b/tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php new file mode 100644 index 0000000000..847936097c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php @@ -0,0 +1,35 @@ +', $foo); + assertType('int', $foo->t); + assertType('int', $foo->u); +}; diff --git a/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php new file mode 100644 index 0000000000..c26933218a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php @@ -0,0 +1,115 @@ + + */ +interface SomePackage extends GenericPackage {} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param TGenericPackage $package + * @return TInnerPackage + */ +function unwrapGeneric(GenericPackage $package) { + $result = $package->unwrap(); + assertType('TInnerPackage of NestedGenericTypesUnwrappingCovariant\InnerPackage (function NestedGenericTypesUnwrappingCovariant\unwrapGeneric(), argument)', $result); + + return $result; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param TGenericPackage $package + * @return TGenericPackage + */ +function unwrapGeneric2(GenericPackage $package) { + assertType('TGenericPackage of NestedGenericTypesUnwrappingCovariant\GenericPackage (function NestedGenericTypesUnwrappingCovariant\unwrapGeneric2(), argument)', $package); + + return $package; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TInnerPackage + */ +function loadWithDirectUnwrap(string $class) { + $package = new $class(); + $result = $package->unwrap(); + assertType('TInnerPackage of NestedGenericTypesUnwrappingCovariant\InnerPackage (function NestedGenericTypesUnwrappingCovariant\loadWithDirectUnwrap(), argument)', $result); + + return $result; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TInnerPackage + */ +function loadWithIndirectUnwrap(string $class) { + $package = new $class(); + $result = unwrapGeneric($package); + assertType('TInnerPackage of NestedGenericTypesUnwrappingCovariant\InnerPackage (function NestedGenericTypesUnwrappingCovariant\loadWithIndirectUnwrap(), argument)', $result); + + return $result; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TGenericPackage + */ +function loadWithIndirectUnwrap2(string $class) { + $package = new $class(); + assertType('TGenericPackage of NestedGenericTypesUnwrappingCovariant\GenericPackage (function NestedGenericTypesUnwrappingCovariant\loadWithIndirectUnwrap2(), argument)', $package); + $result = unwrapGeneric2($package); + assertType('TGenericPackage of NestedGenericTypesUnwrappingCovariant\GenericPackage (function NestedGenericTypesUnwrappingCovariant\loadWithIndirectUnwrap2(), argument)', $result); + + return $result; +} + +function (): void { + $result = loadWithDirectUnwrap(SomePackage::class); + assertType(SomeInnerPackage::class, $result); +}; + +function (): void { + $result = loadWithIndirectUnwrap(SomePackage::class); + assertType(SomeInnerPackage::class, $result); +}; + +function (): void { + $result = loadWithIndirectUnwrap2(SomePackage::class); + assertType(SomePackage::class, $result); +}; + +function (SomePackage $somePackage): void { + $result = unwrapGeneric($somePackage); + assertType(SomeInnerPackage::class, $result); + + $result = unwrapGeneric2($somePackage); + assertType(SomePackage::class, $result); +}; diff --git a/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php new file mode 100644 index 0000000000..567fa07732 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php @@ -0,0 +1,115 @@ + + */ +interface SomePackage extends GenericPackage {} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param TGenericPackage $package + * @return TInnerPackage + */ +function unwrapGeneric(GenericPackage $package) { + $result = $package->unwrap(); + assertType('TInnerPackage of NestedGenericTypesUnwrapping\InnerPackage (function NestedGenericTypesUnwrapping\unwrapGeneric(), argument)', $result); + + return $result; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param TGenericPackage $package + * @return TGenericPackage + */ +function unwrapGeneric2(GenericPackage $package) { + assertType('TGenericPackage of NestedGenericTypesUnwrapping\GenericPackage (function NestedGenericTypesUnwrapping\unwrapGeneric2(), argument)', $package); + + return $package; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TInnerPackage + */ +function loadWithDirectUnwrap(string $class) { + $package = new $class(); + $result = $package->unwrap(); + assertType('TInnerPackage of NestedGenericTypesUnwrapping\InnerPackage (function NestedGenericTypesUnwrapping\loadWithDirectUnwrap(), argument)', $result); + + return $result; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TInnerPackage + */ +function loadWithIndirectUnwrap(string $class) { + $package = new $class(); + $result = unwrapGeneric($package); + assertType('TInnerPackage of NestedGenericTypesUnwrapping\InnerPackage (function NestedGenericTypesUnwrapping\loadWithIndirectUnwrap(), argument)', $result); + + return $result; +} + +/** + * @template TInnerPackage of InnerPackage + * @template TGenericPackage of GenericPackage + * @param class-string $class FQCN to be instantiated + * @return TGenericPackage + */ +function loadWithIndirectUnwrap2(string $class) { + $package = new $class(); + assertType('TGenericPackage of NestedGenericTypesUnwrapping\GenericPackage (function NestedGenericTypesUnwrapping\loadWithIndirectUnwrap2(), argument)', $package); + $result = unwrapGeneric2($package); + assertType('TGenericPackage of NestedGenericTypesUnwrapping\GenericPackage (function NestedGenericTypesUnwrapping\loadWithIndirectUnwrap2(), argument)', $result); + + return $result; +} + +function (): void { + $result = loadWithDirectUnwrap(SomePackage::class); + assertType(SomeInnerPackage::class, $result); +}; + +function (): void { + $result = loadWithIndirectUnwrap(SomePackage::class); + assertType(SomeInnerPackage::class, $result); +}; + +function (): void { + $result = loadWithIndirectUnwrap2(SomePackage::class); + assertType(SomePackage::class, $result); +}; + +function (SomePackage $somePackage): void { + $result = unwrapGeneric($somePackage); + assertType(SomeInnerPackage::class, $result); + + $result = unwrapGeneric2($somePackage); + assertType(SomePackage::class, $result); +}; diff --git a/tests/PHPStan/Analyser/nsrt/nested-generic-types.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types.php new file mode 100644 index 0000000000..179163f1f2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nested-generic-types.php @@ -0,0 +1,166 @@ + + * @template TT of SomeInterface + * @template U + * @template V of SomeInterface + */ +class Foo +{ + + /** @var T */ + public $t; + + /** @var TT */ + public $tt; + + /** @var U */ + public $u; + + /** @var V */ + public $v; + + public function doFoo(): void + { + assertType('T of NestedGenericTypes\SomeInterface (class NestedGenericTypes\Foo, argument)', $this->t); + assertType('TT of NestedGenericTypes\SomeInterface (class NestedGenericTypes\Foo, argument)', $this->tt); + assertType('U (class NestedGenericTypes\Foo, argument)', $this->u); + assertType('V of NestedGenericTypes\SomeInterface (class NestedGenericTypes\Foo, argument)', $this->v); + } + +} + +/** + * @template T of SomeInterface + * @template TT of SomeInterface + * @template U + * @template V of SomeInterface + * @param T $t + * @param TT $tt + * @param U $u + * @param V $v + */ +function testFoo($t, $tt, $u, $v): void +{ + assertType('T of NestedGenericTypes\SomeInterface (function NestedGenericTypes\testFoo(), argument)', $t); + assertType('TT of NestedGenericTypes\SomeInterface (function NestedGenericTypes\testFoo(), argument)', $tt); + assertType('U (function NestedGenericTypes\testFoo(), argument)', $u); + assertType('V of NestedGenericTypes\SomeInterface (function NestedGenericTypes\testFoo(), argument)', $v); +} + +/** @template T */ +interface SomeFoo +{ + +} + +/** @template T */ +interface SomeBar +{ + +} + +/** + * @template T + * @template U of SomeFoo + * @param U $foo + * @return U + */ +function testSome($foo) +{ + +} + +/** + * @template T + * @template U of SomeFoo + * @param U $foo + * @return T + */ +function testSomeUnwrap($foo) +{ + +} + +function (SomeFoo $foo): void +{ + assertType('NestedGenericTypes\SomeFoo', testSome($foo)); + assertType('mixed', testSomeUnwrap($foo)); +}; + +function (SomeBar $bar): void +{ + assertType('NestedGenericTypes\SomeFoo', testSome($bar)); + assertType('mixed', testSomeUnwrap($bar)); +}; + +/** + * @param SomeFoo $foo + */ +function testSome2($foo) +{ + assertType('NestedGenericTypes\SomeFoo', testSome($foo)); + assertType('string', testSomeUnwrap($foo)); +} + +/** + * @param SomeBar $bar + */ +function testSome3($bar) +{ + assertType('NestedGenericTypes\SomeFoo', testSome($bar)); + assertType('mixed', testSomeUnwrap($bar)); +} + +/** + * @template T + * @return T + */ +function unwrapWithoutParam() +{ + +} + +function (): void { + assertType('mixed', unwrapWithoutParam()); +}; + +/** + * @template T of SomeFoo + * @template U + * @return T + */ +function unwrapGenericWithoutParam() +{ + +} + +function (): void { + assertType('NestedGenericTypes\SomeFoo', unwrapGenericWithoutParam()); +}; + +/** + * @template T of SomeFoo + * @param T $t + * @return T + */ +function nonGenericBoundOfGenericClass($t) +{ + return $t; +} + +function (SomeFoo $foo, SomeBar $bar): void { + assertType(SomeFoo::class, nonGenericBoundOfGenericClass($foo)); + assertType(SomeFoo::class, nonGenericBoundOfGenericClass($bar)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/never.php b/tests/PHPStan/Analyser/nsrt/never.php new file mode 100644 index 0000000000..d57a16e5cb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/never.php @@ -0,0 +1,29 @@ += 8.1 + +namespace NeverTest; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(): never + { + exit(); + } + + public function doBar() + { + assertType('never', $this->doFoo()); + } + + public function doBaz(?int $i) + { + if ($i === null) { + $this->doFoo(); + } + + assertType('int', $i); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/new-in-initializers.php b/tests/PHPStan/Analyser/nsrt/new-in-initializers.php new file mode 100644 index 0000000000..091e0ee628 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/new-in-initializers.php @@ -0,0 +1,46 @@ += 8.1 + +namespace NewInInitializers; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @template T of object + * @param T $test + * @return T + */ + public function doFoo( + object $test = new \stdClass() + ): object + { + return $test; + } + + #[\Test(new \stdClass())] + public function doBar() + { + assertType(\stdClass::class, $this->doFoo()); + assertType('$this(NewInInitializers\Foo)', $this->doFoo($this)); + assertType(Bar::class, $this->doFoo(new Bar())); + } + +} + +class Bar extends Foo +{ + + public function doBar() + { + + } + + public function doBaz() + { + static $o = new \stdClass(); + assertType('mixed', $o); + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/non-empty-array-key-type.php b/tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php new file mode 100644 index 0000000000..479fe40846 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php @@ -0,0 +1,37 @@ +', $items); + + if (count($items) > 0) { + assertType('non-empty-array', $items); + foreach ($items as $i => $val) { + assertType('(int|string)', $i); + assertType('stdClass', $val); + } + } + } + + /** + * @param \stdClass[] $items + */ + public function doBar(array $items) + { + foreach ($items as $i => $val) { + assertType('(int|string)', $i); + assertType('stdClass', $val); + } + } + +} 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/nsrt/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php new file mode 100644 index 0000000000..a774b4eba4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php @@ -0,0 +1,34 @@ += 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/nsrt/nullable-closure-parameter.php b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php new file mode 100644 index 0000000000..0758feb78c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php @@ -0,0 +1,24 @@ + $test; + assertType('string|null', $b()); + + fn (string $test = null): string => assertType('string|null', $test); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/nullsafe-vs-scalar.php b/tests/PHPStan/Analyser/nsrt/nullsafe-vs-scalar.php new file mode 100644 index 0000000000..ba26923120 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nullsafe-vs-scalar.php @@ -0,0 +1,34 @@ +getTimestamp() > 0 ? $date->format('j') : ''; + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + assertType('DateTimeImmutable|null', $date); + } + + public function bbb(?\DateTimeImmutable $date): void + { + echo 0 < $date?->getTimestamp() ? $date->format('j') : ''; + echo 0 < $date?->getTimestamp() ? $date->format('j') : ''; + assertType('DateTimeImmutable|null', $date); + } + + /** @param mixed $date */ + public function ccc($date): void + { + if ($date?->getTimestamp() > 0) { + assertType('mixed~null', $date); + } + + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + assertType('mixed', $date); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/nullsafe.php b/tests/PHPStan/Analyser/nsrt/nullsafe.php new file mode 100644 index 0000000000..ed4b00481a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/nullsafe.php @@ -0,0 +1,109 @@ +getMessage()); + assertType('Exception|null', $e); + + assertType('Throwable|null', $e?->getPrevious()); + assertType('string|null', $e?->getPrevious()?->getMessage()); + + $e?->getMessage(assertType('Exception', $e)); + } + + public function doBar(?\ReflectionClass $r) + { + assertType('class-string', $r->name); + assertType('class-string|null', $r?->name); + + assertType('Nullsafe\Foo|null', $this->nullableSelf?->self); + assertType('Nullsafe\Foo|null', $this->nullableSelf?->self->self); + } + + public function doBaz(?self $self) + { + if ($self?->nullableSelf) { + assertType('Nullsafe\Foo', $self); + assertType('Nullsafe\Foo', $self->nullableSelf); + assertType('Nullsafe\Foo', $self?->nullableSelf); + } else { + assertType('Nullsafe\Foo|null', $self); + //assertType('null', $self->nullableSelf); + //assertType('null', $self?->nullableSelf); + } + + assertType('Nullsafe\Foo|null', $self); + assertType('Nullsafe\Foo|null', $self->nullableSelf); + assertType('Nullsafe\Foo|null', $self?->nullableSelf); + } + + public function doLorem(?self $self) + { + if ($self?->nullableSelf !== null) { + assertType('Nullsafe\Foo', $self); + assertType('Nullsafe\Foo', $self->nullableSelf); + assertType('Nullsafe\Foo', $self?->nullableSelf); + } else { + assertType('Nullsafe\Foo|null', $self); + assertType('Nullsafe\Foo|null', $self->nullableSelf); + assertType('null', $self?->nullableSelf); + } + + assertType('Nullsafe\Foo|null', $self); + assertType('Nullsafe\Foo|null', $self->nullableSelf); + assertType('Nullsafe\Foo|null', $self?->nullableSelf); + } + + public function doIpsum(?self $self) + { + if ($self?->nullableSelf === null) { + assertType('Nullsafe\Foo|null', $self); + assertType('Nullsafe\Foo|null', $self); + assertType('null', $self?->nullableSelf); + } else { + assertType('Nullsafe\Foo', $self); + assertType('Nullsafe\Foo', $self->nullableSelf); + assertType('Nullsafe\Foo', $self?->nullableSelf); + } + + assertType('Nullsafe\Foo|null', $self); + assertType('Nullsafe\Foo|null', $self->nullableSelf); + assertType('Nullsafe\Foo|null', $self?->nullableSelf); + } + + public function doDolor(?self $self) + { + if (!$self?->nullableSelf) { + assertType('Nullsafe\Foo|null', $self); + //assertType('null', $self->nullableSelf); + //assertType('null', $self?->nullableSelf); + } else { + assertType('Nullsafe\Foo', $self); + assertType('Nullsafe\Foo', $self->nullableSelf); + assertType('Nullsafe\Foo', $self?->nullableSelf); + } + + assertType('Nullsafe\Foo|null', $self); + assertType('Nullsafe\Foo|null', $self->nullableSelf); + 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/nsrt/number_format.php b/tests/PHPStan/Analyser/nsrt/number_format.php new file mode 100644 index 0000000000..eb4d2a81ca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/number_format.php @@ -0,0 +1,18 @@ +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/nsrt/offset-value-after-assign.php b/tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php new file mode 100644 index 0000000000..1a876b58a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php @@ -0,0 +1,24 @@ + $a + */ + public function doFoo(array $a, int $i): void + { + assertType('string', $a[$i]); + + $a[$i] = 'foo'; + assertType('\'foo\'', $a[$i]); + + $i = 1; + assertType('string', $a[$i]); + } + +} 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 @@ +prepare('DELETE FROM log'); + assertType('(PDOStatement|false)', $logDeleteQuery); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php b/tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php new file mode 100644 index 0000000000..6b02fab73c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php @@ -0,0 +1,15 @@ +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_FLOAT_DIG); +assertType('float', PHP_FLOAT_EPSILON); +assertType('float', PHP_FLOAT_MIN); +assertType('float', PHP_FLOAT_MAX); +assertType('int<1, max>', PHP_FD_SETSIZE); diff --git a/tests/PHPStan/Analyser/nsrt/predefined-constants-php74.php b/tests/PHPStan/Analyser/nsrt/predefined-constants-php74.php new file mode 100644 index 0000000000..e0b7f0d156 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/predefined-constants-php74.php @@ -0,0 +1,7 @@ +', PHP_MAJOR_VERSION); +assertType('int<0, max>', PHP_MINOR_VERSION); +assertType('int<0, max>', PHP_RELEASE_VERSION); +assertType('int<50207, 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/nsrt/preg_match_php7.php b/tests/PHPStan/Analyser/nsrt/preg_match_php7.php new file mode 100644 index 0000000000..0d4887ffea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_php7.php @@ -0,0 +1,12 @@ +|false|null', preg_match_all('{}', '')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_php8.php b/tests/PHPStan/Analyser/nsrt/preg_match_php8.php new file mode 100644 index 0000000000..6261cf789b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_php8.php @@ -0,0 +1,13 @@ += 8.0 + +namespace PregMatchPhp8; + +use function PHPStan\Testing\assertType; + +class Foo { + public function doFoo() { + assertType('0|1|false', preg_match('{}', '')); + assertType('int<0, max>|false', preg_match_all('{}', '')); + + } +} 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/nsrt/proc_get_status.php b/tests/PHPStan/Analyser/nsrt/proc_get_status.php new file mode 100644 index 0000000000..54f63e8094 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/proc_get_status.php @@ -0,0 +1,15 @@ += 8.0 + +namespace PromotedPropertiesTypes; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +/** + * @template T + */ +class Foo +{ + + /** + * @param array $anotherPhpDocArray + * @param T $anotherTemplateProperty + * @param string $bothProperty + * @param array $anotherBothProperty + */ + public function __construct( + public $noType, + public int $nativeIntType, + /** @var array */ public $phpDocArray, + public $anotherPhpDocArray, + /** @var array */ public array $yetAnotherPhpDocArray, + /** @var T */ public $templateProperty, + public $anotherTemplateProperty, + /** @var int */ public $bothProperty, + /** @var array */ public $anotherBothProperty + ) { + assertType('array', $phpDocArray); + assertNativeType('mixed', $phpDocArray); + assertType('array', $anotherPhpDocArray); + assertNativeType('mixed', $anotherPhpDocArray); + assertType('array', $yetAnotherPhpDocArray); + assertNativeType('array', $yetAnotherPhpDocArray); + assertType('int', $bothProperty); + assertType('array', $anotherBothProperty); + } + +} + +function (Foo $foo): void { + assertType('mixed', $foo->noType); + assertType('int', $foo->nativeIntType); + assertType('array', $foo->phpDocArray); + assertType('array', $foo->anotherPhpDocArray); + assertType('array', $foo->yetAnotherPhpDocArray); + assertType('int', $foo->bothProperty); + assertType('array', $foo->anotherBothProperty); +}; + +/** + * @extends Foo<\stdClass> + */ +class Bar extends Foo +{ + +} + +function (Bar $bar): void { + assertType('stdClass', $bar->templateProperty); + assertType('stdClass', $bar->anotherTemplateProperty); +}; + +/** + * @template T + */ +class Lorem +{ + + /** + * @param T $foo + */ + public function __construct( + public $foo + ) { } + +} + +function (): void { + $lorem = new Lorem(new \stdClass); + assertType('stdClass', $lorem->foo); +}; + +/** + * @extends Foo<\stdClass> + */ +class Baz extends Foo +{ + + public function __construct( + public $anotherPhpDocArray + ) + { + assertType('array', $anotherPhpDocArray); + assertNativeType('mixed', $anotherPhpDocArray); + } + +} + +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/nsrt/property-template-tag.php b/tests/PHPStan/Analyser/nsrt/property-template-tag.php new file mode 100644 index 0000000000..11310ed98d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/property-template-tag.php @@ -0,0 +1,21 @@ +, array>>> */ + private array $objectsByKey = array(); + + public function LoadObjectsByKey() : void + { + assertType('array, array>>>', $this->objectsByKey); + } +} diff --git a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php b/tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php similarity index 85% rename from tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php rename to tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php index 875b6d1b47..a2ae0bf2b7 100644 --- a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php +++ b/tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php @@ -2,7 +2,7 @@ namespace PsalmPrefixedTagsWithUnresolvableTypes; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { @@ -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 @@ +', random_int($min, 20)); +}; + +function (int $min) { + \assert($min <= 0); + assertType('int', random_int($min, 20)); +}; + +function (int $max) { + \assert($max >= 0); + assertType('int<0, max>', random_int(0, $max)); +}; + +function (int $i) { + assertType('int', random_int($i, $i)); +}; + +assertType('0', random_int(0, 0)); +assertType('int<-9223372036854775808, 9223372036854775807>', random_int(PHP_INT_MIN, PHP_INT_MAX)); +assertType('int<0, 9223372036854775807>', random_int(0, PHP_INT_MAX)); +assertType('int<-9223372036854775808, 0>', random_int(PHP_INT_MIN, 0)); +assertType('int<-1, 1>', random_int(-1, 1)); +assertType('int<0, 30>', random_int(0, random_int(0, 30))); +assertType('int<0, 100>', random_int(random_int(0, 10), 100)); + +assertType('*NEVER*', random_int(10, 1)); +assertType('*NEVER*', random_int(2, random_int(0, 1))); +assertType('int<0, 1>', random_int(0, random_int(0, 1))); +assertType('*NEVER*', random_int(random_int(0, 1), -1)); +assertType('int<0, 1>', random_int(random_int(0, 1), 1)); + +assertType('int<-5, 5>', random_int(random_int(-5, 0), random_int(0, 5))); +assertType('int<-9223372036854775808, 9223372036854775807>', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); + +assertType('int<-5, 5>', rand(-5, 5)); +assertType('int<0, max>', rand()); diff --git a/tests/PHPStan/Analyser/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/nsrt/range-numeric-string.php b/tests/PHPStan/Analyser/nsrt/range-numeric-string.php new file mode 100644 index 0000000000..bae424e559 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/range-numeric-string.php @@ -0,0 +1,22 @@ +', 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/nsrt/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php new file mode 100644 index 0000000000..800858e827 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php @@ -0,0 +1,23 @@ += 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/nsrt/round.php b/tests/PHPStan/Analyser/nsrt/round.php new file mode 100644 index 0000000000..3d181ca50a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round.php @@ -0,0 +1,61 @@ +', $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 87% rename from tests/PHPStan/Analyser/data/shadowed-trait-methods.php rename to tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php index 2cd7f2d06c..cbddeb4e1d 100644 --- a/tests/PHPStan/Analyser/data/shadowed-trait-methods.php +++ b/tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php @@ -2,7 +2,7 @@ namespace ShadowedTraitMethods; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; trait FooTrait { 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/nsrt/sizeof.php b/tests/PHPStan/Analyser/nsrt/sizeof.php new file mode 100644 index 0000000000..eec773844c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/sizeof.php @@ -0,0 +1,63 @@ +|int<32, max>|string)', $x); + assertType('(int|int<32, max>|string)', $y); + + assertType('bool', $x < $y); + assertType('bool', $x <= $y); + assertType('bool', $x > $y); + assertType('bool', $x >= $y); + + return $x < $y ? 1 : -1; + }); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/sort.php b/tests/PHPStan/Analyser/nsrt/sort.php new file mode 100644 index 0000000000..93dfe0d147 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/sort.php @@ -0,0 +1,153 @@ + 1, + 'five' => 5, + 'three' => 3, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|3|4|5>', $arr1); + assertNativeType('non-empty-list<1|3|4|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|3|4|5>', $arr2); + assertNativeType('non-empty-list<1|3|4|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|3|4|5>', $arr3); + assertNativeType('non-empty-list<1|3|4|5>', $arr3); + } + + public function constantArrayOptionalKey(): void + { + $arr = [ + 'one' => 1, + 'five' => 5, + ]; + if (rand(0, 1)) { + $arr['two'] = 2; + } + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + public function constantArrayUnion(): void + { + $arr = rand(0, 1) ? [ + 'one' => 1, + 'five' => 5, + ] : [ + 'two' => 2, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + /** @param array $arr */ + public function normalArray(array $arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('list', $arr1); + assertNativeType('list', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('list', $arr2); + assertNativeType('list', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('list', $arr3); + assertNativeType('list', $arr3); + } + + public function mixed($arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('mixed', $arr1); + assertNativeType('mixed', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('mixed', $arr2); + assertNativeType('mixed', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('mixed', $arr3); + assertNativeType('mixed', $arr3); + } + + public function notArray(): void + { + $arr = 'foo'; + sort($arr); + assertType("'foo'", $arr); + } +} + +class Bar +{ + + /** + * @template T + * @param T&list $array + * @return list + */ + public function doFoo(array $array) + { + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + usort($array, function (array $a, array $b) { + return $a['a'] <=> $b['a']; + }); + + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + + return $array; + } + +} diff --git a/tests/PHPStan/Analyser/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/nsrt/splfixedarray-iterator-types.php b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php new file mode 100644 index 0000000000..10b3859354 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php @@ -0,0 +1,19 @@ + + */ + public $array; + + public function dump() : void{ + foreach($this->array as $id => $v){ + \PHPStan\Testing\assertType('int|null', $this->array[$id]); + \PHPStan\Testing\assertType('int|null', $v); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/sscanf.php b/tests/PHPStan/Analyser/nsrt/sscanf.php new file mode 100644 index 0000000000..6e88050601 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/sscanf.php @@ -0,0 +1,50 @@ += 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 96% rename from tests/PHPStan/Analyser/data/static-methods.php rename to tests/PHPStan/Analyser/nsrt/static-methods.php index 6df9ea1648..ca689d4bc2 100644 --- a/tests/PHPStan/Analyser/data/static-methods.php +++ b/tests/PHPStan/Analyser/nsrt/static-methods.php @@ -2,7 +2,7 @@ namespace StaticMethods; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { diff --git a/tests/PHPStan/Analyser/data/static-properties.php b/tests/PHPStan/Analyser/nsrt/static-properties.php similarity index 96% rename from tests/PHPStan/Analyser/data/static-properties.php rename to tests/PHPStan/Analyser/nsrt/static-properties.php index 07b2bd149f..bd4312496e 100644 --- a/tests/PHPStan/Analyser/data/static-properties.php +++ b/tests/PHPStan/Analyser/nsrt/static-properties.php @@ -2,7 +2,7 @@ namespace StaticProperties; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { 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/nsrt/strtotime-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/strtotime-return-type-extensions.php new file mode 100644 index 0000000000..b02b16644f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/strtotime-return-type-extensions.php @@ -0,0 +1,29 @@ +', $strtotimeNow); + +$strtotimeInvalid = strtotime('4 qm'); +assertType('false', $strtotimeInvalid); + +$strtotimeUnknown = strtotime(rand(0, 1) === 0 ? 'now': '4 qm'); +assertType('int|false', $strtotimeUnknown); + +$strtotimeUnknown2 = strtotime($undefined); +assertType('(int|false)', $strtotimeUnknown2); + +$strtotimeCrash = strtotime(); +assertType('int|false', $strtotimeCrash); + +$strtotimeWithBase = strtotime('+2 days', time()); +assertType('int', $strtotimeWithBase); + +$strtotimePositiveInt = strtotime('1990-01-01 12:00:00 UTC'); +assertType('int<1, max>', $strtotimePositiveInt); + +$strtotimeNegativeInt = strtotime('1969-12-31 12:00:00 UTC'); +assertType('int', $strtotimeNegativeInt); diff --git a/tests/PHPStan/Analyser/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/nsrt/template-null-bound.php b/tests/PHPStan/Analyser/nsrt/template-null-bound.php new file mode 100644 index 0000000000..3456f02a09 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/template-null-bound.php @@ -0,0 +1,26 @@ +doFoo(null)); + assertType('1', $f->doFoo(1)); + assertType('int|null', $f->doFoo($i)); +}; diff --git a/tests/PHPStan/Analyser/nsrt/ternary-specified-types.php b/tests/PHPStan/Analyser/nsrt/ternary-specified-types.php new file mode 100644 index 0000000000..5a1e915c21 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/ternary-specified-types.php @@ -0,0 +1,39 @@ +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/nsrt/throw-expr.php b/tests/PHPStan/Analyser/nsrt/throw-expr.php new file mode 100644 index 0000000000..2893fe4ee7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-expr.php @@ -0,0 +1,21 @@ += 8.0 + +namespace ThrowExpr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(bool $b): void + { + $result = $b ? true : throw new \Exception(); + assertType('true', $result); + } + + public function doBar(): void + { + assertType('never', throw new \Exception()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/and.php b/tests/PHPStan/Analyser/nsrt/throw-points/and.php new file mode 100644 index 0000000000..645e3775b1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/and.php @@ -0,0 +1,40 @@ + 1, $foo = 1]; + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + [maybeThrows() => 1, $foo = 1]; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php new file mode 100644 index 0000000000..c419633fab --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php @@ -0,0 +1,130 @@ +bar = 'a'; + $foo->bar .= [doesntThrow(), 'b'][1]; + } finally { + assertType('\'ab\'', $foo->bar); + } +}; + +function () { + try { + $foo = new stdClass(); + $foo->bar = 'a'; + $foo->bar .= [maybeThrows(), 'b'][1]; + } finally { + assertType('\'a\'|\'ab\'', $foo->bar); + } +}; + +function () { + try { + $obj = new stdClass(); + $obj->{doesntThrow()} .= ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + $obj = new stdClass(); + $obj->{maybeThrows()} .= ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + try { + Foo::$bar = 'a'; + Foo::$bar .= [doesntThrow(), 'b'][1]; + } finally { + assertType('\'ab\'', Foo::$bar); + } +}; + +function () { + try { + Foo::$bar = 'a'; + Foo::$bar .= [maybeThrows(), 'b'][1]; + } finally { + assertType('\'a\'|\'ab\'', Foo::$bar); + } +}; + +function () { + try { + Foo::${doesntThrow()} .= ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + Foo::${maybeThrows()} .= ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/assign.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign.php new file mode 100644 index 0000000000..13357533a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/assign.php @@ -0,0 +1,170 @@ +bar = false; + $foo->bar = (doesntThrow() || true); + } finally { + assertType('true', $foo->bar); + } +}; + +function () { + try { + $foo = new stdClass(); + $foo->bar = false; + $foo->bar = (maybeThrows() || true); + } finally { + assertType('bool', $foo->bar); + } +}; + +function () { + try { + $obj = new stdClass(); + $obj->{doesntThrow()} = ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + $obj = new stdClass(); + $obj->{maybeThrows()} = ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + try { + Foo::$bar = false; + Foo::$bar = (doesntThrow() || true); + } finally { + assertType('true', Foo::$bar); + } +}; + +function () { + try { + Foo::$bar = false; + Foo::$bar = (maybeThrows() || true); + } finally { + assertType('bool', Foo::$bar); + } +}; + +function () { + try { + Foo::${doesntThrow()} = ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + Foo::${maybeThrows()} = ($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + try { + [$foo] = doesntThrow(); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + [$foo] = maybeThrows(); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + try { + [$foo[doesntThrow()]] = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + [$foo[maybeThrows()]] = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/do-while.php b/tests/PHPStan/Analyser/nsrt/throw-points/do-while.php new file mode 100644 index 0000000000..771ed0de69 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/do-while.php @@ -0,0 +1,29 @@ +throws(); + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj->maybeThrows(); + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj->doesntThrow(); + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + doesntThrow()->{$foo = 1}($bar = 2); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createYes(), $bar); + } +}; + +function () { + try { + maybeThrows()->{$foo = 1}($bar = 2); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $bar); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj->{doesntThrow()}($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj->{maybeThrows()}($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj->doesntThrow(doesntThrow(), $foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj->doesntThrow(maybeThrows(), $foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/or.php b/tests/PHPStan/Analyser/nsrt/throw-points/or.php new file mode 100644 index 0000000000..8be98a68e2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/or.php @@ -0,0 +1,40 @@ +throws(); + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj?->maybeThrows(); + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj?->doesntThrow(); + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + doesntThrow()?->{$foo = 1}($bar = 2); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createYes(), $bar); + } +}; + +function () { + try { + maybeThrows()?->{$foo = 1}($bar = 2); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $bar); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj?->{doesntThrow()}($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj?->{maybeThrows()}($foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj?->doesntThrow(doesntThrow(), $foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + $obj = new ThrowPointTestObject(); + try { + $obj?->doesntThrow(maybeThrows(), $foo = 1); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php new file mode 100644 index 0000000000..ee6cfeae3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php @@ -0,0 +1,26 @@ +foo; + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } +}; + +function () { + try { + maybeThrows()->foo; + $foo = 1; + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/throw-points/static-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/static-call.php new file mode 100644 index 0000000000..0488ecc413 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/throw-points/static-call.php @@ -0,0 +1,68 @@ += 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/nsrt/type-aliases.php b/tests/PHPStan/Analyser/nsrt/type-aliases.php new file mode 100644 index 0000000000..dd7440470f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/type-aliases.php @@ -0,0 +1,219 @@ +baz); + assertType('*ERROR*', $this->qux); + } + + } + + /** + * @phpstan-import-type CircularTypeAliasImport1 from Baz + * @phpstan-type CircularTypeAliasImport2 CircularTypeAliasImport1 + */ + class Qux + { + } + + /** + * @phpstan-type LocalTypeAlias callable(string $value): (string|false) + * @phpstan-type NestedLocalTypeAlias LocalTypeAlias[] + * @phpstan-import-type ExportedTypeAlias from Bar as ImportedTypeAlias + * @phpstan-import-type ReexportedTypeAlias from Baz + * @phpstan-type NestedImportedTypeAlias iterable + * @phpstan-import-type ScopedAlias from SubScope\Bar + * @phpstan-import-type ImportedAliasFromNonClass from int + * @phpstan-import-type ImportedAliasFromUnknownClass from UnknownClass + * @phpstan-import-type ImportedUknownAlias from SubScope\Bar + * @phpstan-type Baz never + * @phpstan-type GlobalTypeAlias never + * @phpstan-type RecursiveTypeAlias RecursiveTypeAlias[] + * @phpstan-type CircularTypeAlias1 CircularTypeAlias2 + * @phpstan-type CircularTypeAlias2 CircularTypeAlias1 + * @phpstan-type int ShouldNotHappen + * @property GlobalTypeAlias $globalAliasProperty + * @property LocalTypeAlias $localAliasProperty + * @property ImportedTypeAlias $importedAliasProperty + * @property ReexportedTypeAlias $reexportedAliasProperty + * @property ScopedAlias $scopedAliasProperty + * @property RecursiveTypeAlias $recursiveAliasProperty + * @property CircularTypeAlias1 $circularAliasProperty + */ + class Foo + { + + /** + * @param GlobalTypeAlias $parameter + */ + public function globalAlias($parameter) + { + assertType('int|string', $parameter); + } + + /** + * @param LocalTypeAlias $parameter + */ + public function localAlias($parameter) + { + assertType('callable(string): (string|false)', $parameter); + } + + /** + * @param NestedLocalTypeAlias $parameter + */ + public function nestedLocalAlias($parameter) + { + assertType('array', $parameter); + } + + /** + * @param ImportedTypeAlias $parameter + */ + public function importedAlias($parameter) + { + assertType('Countable&Traversable', $parameter); + } + + /** + * @param NestedImportedTypeAlias $parameter + */ + public function nestedImportedAlias($parameter) + { + assertType('iterable', $parameter); + } + + /** + * @param ImportedAliasFromNonClass $parameter1 + * @param ImportedAliasFromUnknownClass $parameter2 + * @param ImportedUknownAlias $parameter3 + */ + public function invalidImports($parameter1, $parameter2, $parameter3) + { + assertType('TypeAliasesDataset\ImportedAliasFromNonClass', $parameter1); + assertType('TypeAliasesDataset\ImportedAliasFromUnknownClass', $parameter2); + assertType('TypeAliasesDataset\ImportedUknownAlias', $parameter3); + } + + /** + * @param Baz $parameter + */ + public function conflictingAlias($parameter) + { + assertType('never', $parameter); + } + + public function __get(string $name) + { + return null; + } + + /** @param int $int */ + public function testIntAlias($int) + { + assertType('int', $int); + } + + } + + assertType('int|string', (new Foo)->globalAliasProperty); + 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); + assertType('*ERROR*', (new Foo)->recursiveAliasProperty); + assertType('*ERROR*', (new Foo)->circularAliasProperty); + + trait FooTrait + { + + /** + * @param Test $a + * @return Test + */ + public function doFoo($a) + { + assertType(Test::class, $a); + } + + } + + /** @phpstan-type Test array{string, int} */ + class UsesTrait1 + { + + use FooTrait; + + /** @param Test $a */ + public function doBar($a) + { + assertType('array{string, int}', $a); + assertType(Test::class, $this->doFoo()); + } + + } + + /** @phpstan-type Test \stdClass */ + class UsesTrait2 + { + + use FooTrait; + + /** @param Test $a */ + public function doBar($a) + { + assertType('stdClass', $a); + assertType(Test::class, $this->doFoo()); + } + + } + + class UsesTrait3 + { + + use FooTrait; + + /** @param Test $a */ + public function doBar($a) + { + assertType(Test::class, $a); + assertType(Test::class, $this->doFoo()); + } + + } + +} 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 94% 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 index a7a79c2ffb..863bf44f59 100644 --- a/tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php +++ b/tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php @@ -2,7 +2,7 @@ namespace TypeChangeAfterArrayAccessAssignment; -use function PHPStan\Analyser\assertType; +use function PHPStan\Testing\assertType; class Foo { diff --git a/tests/PHPStan/Analyser/nsrt/uksort-bug.php b/tests/PHPStan/Analyser/nsrt/uksort-bug.php new file mode 100644 index 0000000000..2b5d082018 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uksort-bug.php @@ -0,0 +1,65 @@ + */ + private $stringKeys; + + /** @var array */ + private $intKeys; + + /** + * @template T of array-key + * + * @param array $one + * @param callable(T, T): int $two + * @return T + */ + function uksort( + array &$one, + callable $two + ) { + + } + + public function doFoo(): void + { + assertType('(int|string)', $this->uksort($this->unknownKeys, function (string $a, string $b): int { + return 1; + })); + + assertType('string', $this->uksort($this->stringKeys, function (string $a, string $b): int { + return 1; + })); + assertType('string', $this->uksort($this->stringKeys, function (int $a, int $b): int { + return 1; + })); + assertType('string', $this->uksort($this->stringKeys, function (int $a, string $b): int { + return 1; + })); + + assertType('int', $this->uksort($this->intKeys, function (int $a, int $b): int { + return 1; + })); + assertType('int', $this->uksort($this->intKeys, function (string $a, string $b): int { + return 1; + })); + assertType('int', $this->uksort($this->intKeys, function (int $a, string $b): int { + return 1; + })); + } + +} 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/nsrt/value-of.php b/tests/PHPStan/Analyser/nsrt/value-of.php new file mode 100644 index 0000000000..c05fda0fda --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/value-of.php @@ -0,0 +1,38 @@ + $code + */ + public static function foo(string $code): void + { + assertType('\'jfk\'|\'lga\'', $code); + } + + /** + * @param value-of<'jfk'> $code + */ + public static function bar(string $code): void + { + assertType('string', $code); + } + + /** + * @param value-of<'jfk'|'lga'> $code + */ + public static function baz(string $code): void + { + assertType('string', $code); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/var-above-declare.php b/tests/PHPStan/Analyser/nsrt/var-above-declare.php new file mode 100644 index 0000000000..773a3cfb07 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/var-above-declare.php @@ -0,0 +1,10 @@ += 8.0 + +namespace VariadicParameterPHP8; + +use function PHPStan\Testing\assertType; + +function foo(...$args) +{ + assertType('array', $args); + assertType('mixed', $args['foo']); + assertType('mixed', $args['bar']); +} + +function bar(string ...$args) +{ + assertType('array', $args); +} + 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/nsrt/weird-array_key_exists-issue.php b/tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php new file mode 100644 index 0000000000..b8d7f026ae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php @@ -0,0 +1,51 @@ +> + */ + public function doFoo(array $data): array + { + if (count($data) === 0) { + return []; + } + + arsort($data); + $locationData = []; + $otherData = []; + $i = 0; + $total = array_sum($data); + foreach ($data as $location => $count) { + assertType('int<0, max>', $i); + if ($i < 5) { + $locationData[$location] = [ + 'abs' => $count, + 'rel' => $count / $total * 100, + ]; + } else { + $key = 'Ostatní'; + assertType('bool', array_key_exists($key, $otherData)); + assertType('array<\'Ostatní\', array{abs: int, rel: (float|int)}>', $otherData); + if (!array_key_exists($key, $otherData)) { + $otherData[$key] = [ + 'abs' => 0, + 'rel' => 0, + ]; + assertType('array{Ostatní: array{abs: 0, rel: 0}}', $otherData); + } + $otherData[$key]['abs'] += $count; + $otherData[$key]['rel'] += $count / $total * 100; + assertType('array{Ostatní: array{abs: int, rel: (float|int)}}', $otherData); + } + $i++; + } + + return array_merge($locationData, $otherData); + } +} 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 7b0fa2c07d..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/phpstan.neon b/tests/PHPStan/Analyser/traitsCachingIssue/phpstan.neon deleted file mode 100644 index 799d286c7a..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/phpstan.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - tmpDir: tmp - autoload_directories: - - data 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/typeAliases.neon b/tests/PHPStan/Analyser/typeAliases.neon new file mode 100644 index 0000000000..9f2daa753a --- /dev/null +++ b/tests/PHPStan/Analyser/typeAliases.neon @@ -0,0 +1,3 @@ +parameters: + typeAliases: + GlobalTypeAlias: int|string 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 b97cf902c8..0000000000 --- a/tests/PHPStan/Broker/BrokerTest.php +++ /dev/null @@ -1,95 +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([], []); - $dynamicReturnTypeExtensionRegistryProvider = new DirectDynamicReturnTypeExtensionRegistryProvider([], [], []); - $operatorTypeSpecifyingExtensionRegistryProvider = new DirectOperatorTypeSpecifyingExtensionRegistryProvider([]); - - $setterReflectionProviderProvider = new SetterReflectionProviderProvider(); - $reflectionProvider = new RuntimeReflectionProvider( - $setterReflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $this->createMock(FunctionReflectionFactory::class), - new FileTypeMapper($setterReflectionProviderProvider, $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), $anonymousClassNameHelper), - self::getContainer()->getByType(NativeFunctionReflectionProvider::class), - self::getContainer()->getByType(\PhpParser\PrettyPrinter\Standard::class), - $anonymousClassNameHelper, - $fileHelper, - $relativePathHelper, - self::getContainer()->getByType(StubPhpDocProvider::class) - ); - $setterReflectionProviderProvider->setReflectionProvider($reflectionProvider); - $this->broker = new Broker( - $reflectionProvider, - $dynamicReturnTypeExtensionRegistryProvider, - $operatorTypeSpecifyingExtensionRegistryProvider, - [] - ); - $classReflectionExtensionRegistryProvider->setBroker($this->broker); - $dynamicReturnTypeExtensionRegistryProvider->setBroker($this->broker); - $operatorTypeSpecifyingExtensionRegistryProvider->setBroker($this->broker); - } - - public function testClassNotFound(): void - { - $this->expectException(\PHPStan\Broker\ClassNotFoundException::class); - $this->expectExceptionMessage('NonexistentClass'); - $this->broker->getClass('NonexistentClass'); - } - - public function testFunctionNotFound(): void - { - $this->expectException(\PHPStan\Broker\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(\PHPStan\Broker\ClassAutoloadingException::class); - $this->expectExceptionMessage("ParseError (syntax error, unexpected '{') 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 ed6b08a4f9..e84ac78e28 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -2,15 +2,27 @@ namespace PHPStan\Command; -use PHPStan\Analyser\ResultCache\ResultCacheManager; +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 fopen; +use function rewind; +use function sprintf; +use function stream_get_contents; +use const DIRECTORY_SEPARATOR; -class AnalyseApplicationIntegrationTest extends \PHPStan\Testing\TestCase +class AnalyseApplicationIntegrationTest extends PHPStanTestCase { public function testExecuteOnAFile(): void @@ -25,7 +37,7 @@ public function testExecuteOnANonExistentPath(): void $output = $this->runPath($path, 1); $this->assertStringContainsString(sprintf( 'File %s does not exist.', - $path + $path, ), $output); } @@ -38,46 +50,53 @@ public function testExecuteOnAFileWithErrors(): void private function runPath(string $path, int $expectedStatusCode): string { - self::getContainer()->getByType(ResultCacheManager::class)->clear(); + self::getContainer()->getByType(ResultCacheClearer::class)->clear(); $analyserApplication = self::getContainer()->getByType(AnalyseApplication::class); $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $output = new StreamOutput($resource); $symfonyOutput = new SymfonyOutput( $output, - new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createMock(InputInterface::class), $output)) + new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createMock(InputInterface::class), $output)), ); - $memoryLimitFile = self::getContainer()->getParameter('memoryLimitFile'); - - $relativePathHelper = new FuzzyRelativePathHelper(__DIR__, [], DIRECTORY_SEPARATOR); - $errorFormatter = new TableErrorFormatter($relativePathHelper, false); + $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), __DIR__, [], DIRECTORY_SEPARATOR); + $errorFormatter = new TableErrorFormatter( + $relativePathHelper, + new SimpleRelativePathHelper(__DIR__), + new CiDetectedErrorFormatter( + new GithubErrorFormatter($relativePathHelper), + new TeamcityErrorFormatter($relativePathHelper), + ), + false, + null, + null, + ); $analysisResult = $analyserApplication->analyse( [$path], true, $symfonyOutput, $symfonyOutput, false, - false, + true, + null, null, - $this->createMock(InputInterface::class) + $this->createMock(InputInterface::class), ); - if (file_exists($memoryLimitFile)) { - unlink($memoryLimitFile); - } $statusCode = $errorFormatter->formatErrors($analysisResult, $symfonyOutput); - $this->assertSame($expectedStatusCode, $statusCode); rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + 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 69da8c997f..f47edea747 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -2,32 +2,39 @@ namespace PHPStan\Command; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; use Symfony\Component\Console\Tester\CommandTester; +use Throwable; +use function chdir; +use function getcwd; +use function microtime; +use function realpath; +use function sprintf; use const DIRECTORY_SEPARATOR; +use const PHP_EOL; /** * @group exec */ -class AnalyseCommandTest extends \PHPStan\Testing\TestCase +class AnalyseCommandTest extends PHPStanTestCase { /** - * @param string $dir - * @param string $file * @dataProvider autoDiscoveryPathsProvider */ public function testConfigurationAutoDiscovery(string $dir, string $file): void { $originalDir = getcwd(); if ($originalDir === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } chdir($dir); try { $output = $this->runCommand(1); $this->assertStringContainsString('Note: Using configuration file ' . $file . '.', $output); - } catch (\Throwable $e) { + } catch (Throwable $e) { chdir($originalDir); throw $e; } @@ -44,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; + } } /** @@ -59,34 +78,57 @@ 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-dist', - __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-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-no-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-no-dot' . DIRECTORY_SEPARATOR . 'phpstan.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', __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority' . DIRECTORY_SEPARATOR . 'phpstan.neon', ], + [ + __DIR__ . '/test-autodiscover-priority-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.neon', + ], + [ + __DIR__ . '/test-autodiscover-priority-dot', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dot' . DIRECTORY_SEPARATOR . '.phpstan.neon', + ], ]; } /** - * @param int $expectedStatusCode * @param array $parameters - * @return string */ 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 c068a49f18..2ea3344a9b 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -3,9 +3,9 @@ namespace PHPStan\Command; use PHPStan\Analyser\Error; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -final class AnalysisResultTest extends TestCase +final class AnalysisResultTest extends PHPStanTestCase { public function testErrorsAreSortedByFileNameAndLine(): void @@ -38,10 +38,15 @@ public function testErrorsAreSortedByFileNameAndLine(): void ], [], [], + [], + [], false, null, - false - ))->getFileSpecificErrors() + true, + 0, + false, + [], + ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index 3c1f5cc3f7..ab4b4b793a 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -2,10 +2,16 @@ namespace PHPStan\Command; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\StreamOutput; +use function fopen; +use function realpath; +use function rewind; +use function stream_get_contents; +use const DIRECTORY_SEPARATOR; /** * @group exec @@ -89,12 +95,7 @@ public function dataBegin(): array /** * @dataProvider dataBegin - * @param string $input - * @param string $expectedOutput - * @param string|null $projectConfigFile - * @param string|null $level * @param mixed[] $expectedParameters - * @param bool $expectException */ public function testBegin( string $input, @@ -102,12 +103,12 @@ public function testBegin( ?string $projectConfigFile, ?string $level, array $expectedParameters, - bool $expectException + bool $expectException, ): void { $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $output = new StreamOutput($resource); @@ -118,23 +119,23 @@ public function testBegin( [__DIR__], null, null, - null, [], $projectConfigFile, null, $level, false, - true + false, + false, ); if ($expectException) { $this->fail(); } - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { if (!$expectException) { rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $this->fail($contents); } @@ -144,7 +145,7 @@ public function testBegin( $contents = stream_get_contents($output->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $this->assertStringContainsString($expectedOutput, $contents); @@ -159,26 +160,19 @@ public function testBegin( } } - public function dataResolveRelativePaths(): array + public function dataParameters(): array { return [ [ __DIR__ . '/relative-paths/root.neon', [ - 'bootstrap' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', 'bootstrapFiles' => [ + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'), + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionAttribute.php'), + realpath(__DIR__ . '/../../../stubs/runtime/Attribute.php'), + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionIntersectionType.php'), __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', ], - 'autoload_files' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', - __DIR__ . DIRECTORY_SEPARATOR . 'up.php', - ], - 'autoload_directories' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths', - realpath(__DIR__ . '/../../../') . '/conf', - ], 'scanFiles' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', @@ -192,22 +186,25 @@ public function dataResolveRelativePaths(): array 'paths' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', ], - 'memoryLimitFile' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . '.memory_limit', - 'excludes_analyse' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . 'data', - '*/src/*/data', + 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', + __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . 'data', + '*/src/*/data', + ], + 'analyse' => [], ], ], ], [ __DIR__ . '/relative-paths/nested/nested.neon', [ - 'autoload_files' => [ + 'scanFiles' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'nested' . DIRECTORY_SEPARATOR . 'here.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'nested' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'up.php', ], + 'reportUnmatchedIgnoredErrors' => false, 'ignoreErrors' => [ [ 'message' => '#aaa#', @@ -223,17 +220,82 @@ public function dataResolveRelativePaths(): array ], ], ], + [ + __DIR__ . '/exclude-paths/straightforward.neon', + [ + 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', + ], + 'analyse' => [], + ], + ], + ], + [ + __DIR__ . '/exclude-paths/full.neon', + [ + 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', + ], + 'analyse' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + ], + ], + ], + ], + [ + __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', + ], + ], + ], + ], + [ + __DIR__ . '/exclude-paths/including-mixed.neon', + [ + 'excludePaths' => [ + 'analyseAndScan' => [ + '*.blade.php', + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', + ], + 'analyse' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + ], + ], + ], + ], + [ + __DIR__ . '/exclude-paths/including-mixed-vice-versa.neon', + [ + 'excludePaths' => [ + 'analyseAndScan' => [ + '*.blade.php', + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + ], + 'analyse' => [], + ], + ], + ], ]; } /** - * @dataProvider dataResolveRelativePaths - * @param string $configFile + * @dataProvider dataParameters * @param array $expectedParameters + * @throws InceptionNotSuccessfulException */ - public function testResolveRelativePaths( + public function testResolveParameters( string $configFile, - array $expectedParameters + array $expectedParameters, ): void { $result = CommandHelper::begin( @@ -242,13 +304,13 @@ public function testResolveRelativePaths( [__DIR__], null, null, - null, [], $configFile, null, '0', false, - true + false, + false, ); $parameters = $result->getContainer()->getParameters(); foreach ($expectedParameters as $name => $expectedValue) { diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php index 67ef1e8839..07c6d9ffe2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php @@ -3,10 +3,17 @@ namespace PHPStan\Command\ErrorFormatter; use Nette\Utils\Json; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; +use function array_sum; use function chdir; -use function file_put_contents; +use function escapeshellarg; +use function exec; use function getcwd; +use function implode; +use function sprintf; +use function unlink; +use const PHP_BINARY; /** * @group exec @@ -16,7 +23,7 @@ class BaselineNeonErrorFormatterIntegrationTest extends TestCase public function testErrorWithTrait(): void { - $output = $this->runPhpStan(__DIR__ . '/data/', null); + $output = $this->runPhpStan(__DIR__ . '/data/', __DIR__ . '/empty.neon'); $errors = Json::decode($output, Json::FORCE_ARRAY); $this->assertSame(10, array_sum($errors['totals'])); $this->assertCount(6, $errors['files']); @@ -24,9 +31,8 @@ public function testErrorWithTrait(): void public function testGenerateBaselineAndRunAgainWithIt(): void { - $output = $this->runPhpStan(__DIR__ . '/data/', null, 'baselineNeon'); $baselineFile = __DIR__ . '/../../../../baseline.neon'; - file_put_contents($baselineFile, $output); + $output = $this->runPhpStan(__DIR__ . '/data/', __DIR__ . '/empty.neon', 'json', $baselineFile); $output = $this->runPhpStan(__DIR__ . '/data/', $baselineFile); @unlink($baselineFile); @@ -54,20 +60,21 @@ public function testRunUnixFileWithWindowsBaseline(): void private function runPhpStan( string $analysedPath, ?string $configFile, - string $errorFormatter = 'json' + string $errorFormatter = 'json', + ?string $baselineFile = null, ): string { $originalDir = getcwd(); if ($originalDir === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } chdir(__DIR__ . '/../../../..'); exec(sprintf('%s %s clear-result-cache %s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : ''), $clearResultCacheOutputLines, $clearResultCacheExitCode); if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache.'); + throw new ShouldNotHappenException('Could not clear result cache.'); } - exec(sprintf('%s %s analyse --no-progress --error-format=%s --level=7 %s %s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $errorFormatter, $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : '', escapeshellarg($analysedPath)), $outputLines); + exec(sprintf('%s %s analyse --no-progress --error-format=%s --level=7 %s %s%s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $errorFormatter, $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : '', escapeshellarg($analysedPath), $baselineFile !== null ? ' --generate-baseline ' . escapeshellarg($baselineFile) : ''), $outputLines); chdir($originalDir); return implode("\n", $outputLines); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index 8a2d85d77f..ace5a21c5b 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -2,11 +2,28 @@ namespace PHPStan\Command\ErrorFormatter; +use Generator; 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 { @@ -42,7 +59,7 @@ public function dataFormatterOutputProvider(): iterable 0, [ [ - 'message' => '#^Bar$#', + 'message' => "#^Bar\nBar2$#", 'count' => 1, 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', ], @@ -52,12 +69,12 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', ], [ - 'message' => '#^Foo$#', + 'message' => "#^Bar\nBar2$#", 'count' => 1, 'path' => 'foo.php', ], [ - 'message' => '#^Bar$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -71,7 +88,7 @@ public function dataFormatterOutputProvider(): iterable 2, [ [ - 'message' => '#^Bar$#', + 'message' => "#^Bar\nBar2$#", 'count' => 1, 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', ], @@ -81,12 +98,12 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', ], [ - 'message' => '#^Foo$#', + 'message' => "#^Bar\nBar2$#", 'count' => 1, 'path' => 'foo.php', ], [ - 'message' => '#^Bar$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -97,10 +114,6 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors * @param mixed[] $expected */ public function testFormatErrors( @@ -108,20 +121,20 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, - array $expected + array $expected, ): void { $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), + '', ), sprintf('%s: response code do not match', $message)); $this->assertSame(trim(Neon::encode(['parameters' => ['ignoreErrors' => $expected]], Neon::BLOCK)), trim($this->getOutputContent()), sprintf('%s: output do not match', $message)); } - public function testFormatErrorMessagesRegexEscape(): void { $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); @@ -130,13 +143,19 @@ public function testFormatErrorMessagesRegexEscape(): void [new Error('Escape Regex with file # ~ \' ()', 'Testfile')], ['Escape Regex without file # ~ <> \' ()'], [], + [], + [], false, null, - false + true, + 0, + false, + [], ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), + '', ); self::assertSame( @@ -151,10 +170,415 @@ public function testFormatErrorMessagesRegexEscape(): void ], ], ], - ], Neon::BLOCK) + ], Neon::BLOCK), ), - trim($this->getOutputContent()) + trim($this->getOutputContent()), + ); + } + + public function testEscapeDiNeon(): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $result = new AnalysisResult( + [new Error('Test %value%', 'Testfile')], + [], + [], + [], + [], + false, + null, + true, + 0, + false, + [], + ); + + $formatter->formatErrors( + $result, + $this->getOutput(), + '', + ); + self::assertSame( + trim( + Neon::encode([ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'message' => '#^Test %%value%%$#', + 'count' => 1, + 'path' => 'Testfile', + ], + ], + ], + ], Neon::BLOCK), + ), + trim($this->getOutputContent()), + ); + } + + /** + * @return Generator}, void, void> + */ + public function outputOrderingProvider(): Generator + { + $errors = [ + new Error('Error #2', 'TestfileA', 1), + new Error('A different error #1', 'TestfileA', 3), + new Error('Second error in a different file', 'TestfileB', 4), + new Error('Error #1 in a different file', 'TestfileB', 5), + new Error('Second error in a different file', 'TestfileB', 6), + new Error('Error with Windows directory separators', 'TestFiles\\TestA', 1), + new Error('Error with Unix directory separators', 'TestFiles/TestA', 1), + new Error('Error without directory separators', 'TestFilesFoo', 1), + ]; + yield [$errors]; + mt_srand(0); + for ($i = 0; $i < 3; ++$i) { + shuffle($errors); + yield [$errors]; + } + } + + /** + * @dataProvider outputOrderingProvider + * @param list $errors + */ + public function testOutputOrdering(array $errors): void + { + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $result = new AnalysisResult( + $errors, + [], + [], + [], + [], + false, + null, + true, + 0, + false, + [], ); + + $formatter->formatErrors( + $result, + $this->getOutput(), + '', + ); + self::assertSame( + trim(Neon::encode([ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'message' => '#^Error with Unix directory separators$#', + 'count' => 1, + 'path' => 'TestFiles/TestA', + ], + [ + 'message' => '#^Error with Windows directory separators$#', + 'count' => 1, + 'path' => 'TestFiles/TestA', + ], + [ + 'message' => '#^Error without directory separators$#', + 'count' => 1, + 'path' => 'TestFilesFoo', + ], + [ + 'message' => '#^A different error \\#1$#', + 'count' => 1, + 'path' => 'TestfileA', + ], + [ + 'message' => '#^Error \\#2$#', + 'count' => 1, + 'path' => 'TestfileA', + ], + [ + 'message' => '#^Error \\#1 in a different file$#', + 'count' => 1, + 'path' => 'TestfileB', + ], + [ + 'message' => '#^Second error in a different file$#', + 'count' => 2, + 'path' => 'TestfileB', + ], + ], + ], + ], Neon::BLOCK)), + $f = trim($this->getOutputContent()), + ); + } + + /** + * @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 025daf55de..6618b6effe 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -6,6 +6,7 @@ use PHPStan\Command\AnalysisResult; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class CheckstyleErrorFormatterTest extends ErrorFormatterTestCase { @@ -59,12 +60,12 @@ public function dataFormatterOutputProvider(): iterable ' - + - - + + ', @@ -79,7 +80,7 @@ public function dataFormatterOutputProvider(): iterable - + ', @@ -93,16 +94,16 @@ public function dataFormatterOutputProvider(): iterable ' - + - - + + - + ', @@ -112,25 +113,20 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new CheckstyleErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $outputContent = $this->getOutputContent(); @@ -147,15 +143,20 @@ public function testTraitPath(): void 5, true, __DIR__ . '/Foo.php', - __DIR__ . '/FooTrait.php' + __DIR__ . '/FooTrait.php', ); $formatter->formatErrors(new AnalysisResult( [$error], [], [], + [], + [], false, null, - false + true, + 0, + false, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' @@ -164,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 new file mode 100644 index 0000000000..3a93977ce5 --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php @@ -0,0 +1,103 @@ + +::error file=foo.php,line=5,col=0::Bar%0ABar2 +', + ]; + + yield [ + 'Multiple generic errors', + 1, + 0, + 2, + '::error ::first generic error +::error ::second generic +', + ]; + + yield [ + 'Multiple file, multiple generic errors', + 1, + 4, + 2, + '::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=5,col=0::Bar%0ABar2 +::error ::first generic error +::error ::second generic +', + ]; + } + + /** + * @dataProvider dataFormatterOutputProvider + * + */ + public function testFormatErrors( + string $message, + int $exitCode, + int $numFileErrors, + int $numGenericErrors, + string $expected, + ): void + { + $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); + $formatter = new GithubErrorFormatter( + $relativePathHelper, + ); + + $this->assertSame($exitCode, $formatter->formatErrors( + $this->getAnalysisResult($numFileErrors, $numGenericErrors), + $this->getOutput(), + ), sprintf('%s: response code do not match', $message)); + + $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + } + +} diff --git a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php index 3a045443f6..78df3d79dd 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php @@ -4,6 +4,7 @@ use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class GitlabFormatterTest extends ErrorFormatterTestCase { @@ -27,6 +28,7 @@ public function dataFormatterOutputProvider(): iterable { "description": "Foo", "fingerprint": "e82b7e1f1d4255352b19ecefa9116a12f129c7edb4351cf2319285eccdb1565e", + "severity": "major", "location": { "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", "lines": { @@ -46,6 +48,7 @@ public function dataFormatterOutputProvider(): iterable { "description": "first generic error", "fingerprint": "53ed216d77c9a9b21d9535322457ca7d7b037d6596d76484b3481f161adfd96f", + "severity": "major", "location": { "path": "", "lines": { @@ -63,8 +66,9 @@ public function dataFormatterOutputProvider(): iterable 0, '[ { - "description": "Bar", - "fingerprint": "d112f1651daa597592156359ef28c9a4b81a8a96dbded1c0f1009f5bbc2bda97", + "description": "Bar\nBar2", + "fingerprint": "034b4afbfb347494c14e396ed8327692f58be4cd27e8aff5f19f4194934db7c9", + "severity": "major", "location": { "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", "lines": { @@ -75,6 +79,7 @@ public function dataFormatterOutputProvider(): iterable { "description": "Foo", "fingerprint": "e82b7e1f1d4255352b19ecefa9116a12f129c7edb4351cf2319285eccdb1565e", + "severity": "major", "location": { "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", "lines": { @@ -82,9 +87,74 @@ public function dataFormatterOutputProvider(): iterable } } }, + { + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", + "severity": "major", + "location": { + "path": "with space/and unicode \ud83d\ude03/project/foo.php", + "lines": { + "begin": 1 + } + } + }, + { + "description": "Bar\nBar2", + "fingerprint": "829f6c782152fdac840b39208c5b519d18e51bff2c601b6197812fffb8bcd9ed", + "severity": "major", + "location": { + "path": "with space/and unicode \ud83d\ude03/project/foo.php", + "lines": { + "begin": 5 + } + } + } +]', + ]; + + yield [ + 'Multiple file errors, including error with line=null', + 1, + 5, + 0, + '[ + { + "description": "Bar\nBar2", + "fingerprint": "034b4afbfb347494c14e396ed8327692f58be4cd27e8aff5f19f4194934db7c9", + "severity": "major", + "location": { + "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", + "lines": { + "begin": 2 + } + } + }, { "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "fingerprint": "e82b7e1f1d4255352b19ecefa9116a12f129c7edb4351cf2319285eccdb1565e", + "severity": "major", + "location": { + "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", + "lines": { + "begin": 4 + } + } + }, + { + "description": "Bar\nBar2", + "fingerprint": "52d22d9e64bd6c6257b7a0d170ed8c99482043aeedd68c52bac081a80da9800a", + "severity": "major", + "location": { + "path": "with space/and unicode \ud83d\ude03/project/foo.php", + "lines": { + "begin": 0 + } + } + }, + { + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", + "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", "lines": { @@ -93,8 +163,9 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Bar", - "fingerprint": "d83022ee5bc7c71b6a4988ec47a377c9998b929d12d86fc71b745ec2b04c81e5", + "description": "Bar\nBar2", + "fingerprint": "829f6c782152fdac840b39208c5b519d18e51bff2c601b6197812fffb8bcd9ed", + "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", "lines": { @@ -114,6 +185,7 @@ public function dataFormatterOutputProvider(): iterable { "description": "first generic error", "fingerprint": "53ed216d77c9a9b21d9535322457ca7d7b037d6596d76484b3481f161adfd96f", + "severity": "major", "location": { "path": "", "lines": { @@ -122,8 +194,9 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", + "severity": "major", "location": { "path": "", "lines": { @@ -141,8 +214,9 @@ public function dataFormatterOutputProvider(): iterable 2, '[ { - "description": "Bar", - "fingerprint": "d112f1651daa597592156359ef28c9a4b81a8a96dbded1c0f1009f5bbc2bda97", + "description": "Bar\nBar2", + "fingerprint": "034b4afbfb347494c14e396ed8327692f58be4cd27e8aff5f19f4194934db7c9", + "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/folder with unicode \ud83d\ude03/file name with \"spaces\" and unicode \ud83d\ude03.php", "lines": { @@ -153,6 +227,7 @@ public function dataFormatterOutputProvider(): iterable { "description": "Foo", "fingerprint": "e82b7e1f1d4255352b19ecefa9116a12f129c7edb4351cf2319285eccdb1565e", + "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/folder with unicode \ud83d\ude03/file name with \"spaces\" and unicode \ud83d\ude03.php", "lines": { @@ -161,8 +236,9 @@ 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", "lines": { @@ -171,8 +247,9 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Bar", - "fingerprint": "d83022ee5bc7c71b6a4988ec47a377c9998b929d12d86fc71b745ec2b04c81e5", + "description": "Bar\nBar2", + "fingerprint": "829f6c782152fdac840b39208c5b519d18e51bff2c601b6197812fffb8bcd9ed", + "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", "lines": { @@ -183,6 +260,7 @@ public function dataFormatterOutputProvider(): iterable { "description": "first generic error", "fingerprint": "53ed216d77c9a9b21d9535322457ca7d7b037d6596d76484b3481f161adfd96f", + "severity": "major", "location": { "path": "", "lines": { @@ -191,8 +269,9 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", + "severity": "major", "location": { "path": "", "lines": { @@ -207,11 +286,6 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected * */ public function testFormatErrors( @@ -219,14 +293,14 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new GitlabErrorFormatter(new SimpleRelativePathHelper('/data/folder')); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index dd6a3e82e6..ff1626d7d2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Command\ErrorFormatter; +use Nette\Utils\Json; +use PHPStan\Analyser\Error; +use PHPStan\Command\AnalysisResult; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class JsonErrorFormatterTest extends ErrorFormatterTestCase { @@ -20,7 +24,7 @@ public function dataFormatterOutputProvider(): iterable "errors":0, "file_errors":0 }, - "files":[], + "files":{}, "errors": [] }', ]; @@ -63,7 +67,7 @@ public function dataFormatterOutputProvider(): iterable "errors":1, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error" ] @@ -86,7 +90,7 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Bar", + "message": "Bar\nBar2", "line": 2, "ignorable": true }, @@ -101,14 +105,15 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, { - "message": "Bar", + "message": "Bar\nBar2", "line": 5, - "ignorable": true + "ignorable": true, + "tip": "a tip" } ] } @@ -128,10 +133,10 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "file_errors":0 }, - "files":[], + "files":{}, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -152,7 +157,7 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Bar", + "message": "Bar\nBar2", "line": 2, "ignorable": true }, @@ -167,21 +172,22 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, { - "message": "Bar", + "message": "Bar\nBar2", "line": 5, - "ignorable": true + "ignorable": true, + "tip": "a tip" } ] } }, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -190,25 +196,20 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testPrettyFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new JsonErrorFormatter(true); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), $message); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent()); @@ -217,11 +218,6 @@ public function testPrettyFormatErrors( /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected * */ public function testFormatErrors( @@ -229,17 +225,39 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new JsonErrorFormatter(false); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: JSON do not match', $message)); } + 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 b3495ca738..f83f162bb2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php @@ -10,8 +10,7 @@ class JunitErrorFormatterTest extends ErrorFormatterTestCase { - /** @var \PHPStan\Command\ErrorFormatter\JunitErrorFormatter */ - private $formatter; + private JunitErrorFormatter $formatter; public function setUp(): void { @@ -21,7 +20,7 @@ public function setUp(): void } /** - * @return \Generator> + * @return Generator> */ public function dataFormatterOutputProvider(): Generator { @@ -30,7 +29,7 @@ public function dataFormatterOutputProvider(): Generator 0, 0, ' - + ', @@ -69,16 +68,16 @@ public function dataFormatterOutputProvider(): Generator ' - + - + - + ', @@ -94,7 +93,7 @@ public function dataFormatterOutputProvider(): Generator - + ', @@ -107,22 +106,22 @@ public function dataFormatterOutputProvider(): Generator ' - + - + - + - + ', @@ -138,30 +137,30 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGeneralErrors, - string $expected + string $expected, ): void { $this->assertSame( $exitCode, $this->formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGeneralErrors), - $this->getOutput() + $this->getOutput(), ), - 'Response code do not match' + 'Response code do not match', ); $xml = new DOMDocument(); $xml->loadXML($this->getOutputContent()); $this->assertTrue( - $xml->schemaValidate('https://raw.githubusercontent.com/junit-team/junit5/r5.5.1/platform-tests/src/test/resources/jenkins-junit.xsd'), - 'Schema do not validate' + $xml->schemaValidate(__DIR__ . '/junit-schema.xsd'), + 'Schema do not validate', ); $this->assertXmlStringEqualsXmlString( $expected, $this->getOutputContent(), - 'XML do not match' + 'XML do not match', ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php index 023f7d0164..ffe1c436d3 100644 --- a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Command\ErrorFormatter; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class RawErrorFormatterTest extends ErrorFormatterTestCase { @@ -10,88 +11,117 @@ 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' . "\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' . "\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' . "\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' . "\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 string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected + * @param array{int, int}|int $numFileErrors */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, - string $expected + bool $verbose, + string $expected, ): void { $formatter = new RawErrorFormatter(); $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 a54bb5c90e..1ada3c4624 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -2,35 +2,58 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; +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; 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 - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- + [ERROR] Found 1 error @@ -38,39 +61,47 @@ 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 -- --------------------- + [ERROR] Found 1 error ', ]; 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 + Bar2 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- - ------ --------- + ------ ---------- Line foo.php - ------ --------- - 1 Foo + ------ ---------- + 1 Foo 5 Bar - ------ --------- + Bar2 + 💡 a tip + ------ ---------- [ERROR] Found 4 errors @@ -78,16 +109,19 @@ 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 @@ -95,62 +129,208 @@ 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 + Bar2 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 string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected + * @param array{int, int}|int $numFileErrors + * @param array $extraEnvVars */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, - string $expected + bool $verbose, + array $extraEnvVars, + string $expected, ): void { - $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(self::DIRECTORY_PATH, [], '/'), false); + $formatter = $this->createErrorFormatter(null); + + // NOTE: extra env vars need to be cleared in tearDown() + foreach ($extraEnvVars as $envVar) { + putenv($envVar); + } $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $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 = $this->createErrorFormatter('editor://%file%/%line%'); + $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, 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 = $this->createErrorFormatter(null); + $formatter->formatErrors( + new AnalysisResult( + [ + new Error( + 'Method MissingTypehintPromotedProperties\Foo::__construct() has parameter $foo with no value type specified in iterable type array.', + '/var/www/html/app/src/Foo.php (in context of class App\Foo\Bar)', + 5, + ), + ], + [], + [], + [], + [], + false, + null, + true, + 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 new file mode 100644 index 0000000000..6543fbae67 --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php @@ -0,0 +1,120 @@ +\' 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\'] +', + ]; + + yield [ + 'Multiple generic errors', + 1, + 0, + 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\' file=\'.\' SEVERITY=\'ERROR\'] +', + ]; + + yield [ + 'Multiple file, multiple generic errors', + 1, + 4, + 2, + '##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=\'a tip\'] +##teamcity[inspection typeId=\'phpstan\' message=\'first 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, + array|int $numFileErrors, + int $numGenericErrors, + string $expected, + ): void + { + $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); + $formatter = new TeamcityErrorFormatter( + $relativePathHelper, + ); + + $this->assertSame($exitCode, $formatter->formatErrors( + $this->getAnalysisResult($numFileErrors, $numGenericErrors), + $this->getOutput(), + ), sprintf('%s: response code do not match', $message)); + + $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + } + +} diff --git a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon index 698fd971d5..ff39bfc9d7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon @@ -1,17 +1,20 @@ parameters: ignoreErrors: - - message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has no return typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has no return type specified\\.$#" count: 1 path: WindowsNewlines.php - - message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no type specified\\.$#" count: 1 path: WindowsNewlines.php - - 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 bce7fed185..398e241bd7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon @@ -1,17 +1,20 @@ parameters: ignoreErrors: - - message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has no return typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has no return type specified\\.$#" count: 1 path: UnixNewlines.php - - message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no type specified\\.$#" count: 1 path: UnixNewlines.php - - 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/ErrorFormatter/empty.neon b/tests/PHPStan/Command/ErrorFormatter/empty.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/PHPStan/Command/ErrorFormatter/junit-schema.xsd b/tests/PHPStan/Command/ErrorFormatter/junit-schema.xsd new file mode 100644 index 0000000000..b9b70088a0 --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/junit-schema.xsd @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index eb8110e54f..39902aa01f 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Command; +use Hoa\Compiler\Llk\Llk; +use Hoa\File\Read; use PHPStan\PhpDoc\TypeStringResolver; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -class IgnoredRegexValidatorTest extends TestCase +class IgnoredRegexValidatorTest extends PHPStanTestCase { public function dataValidate(): array @@ -98,31 +100,70 @@ 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#', + [], + false, + true, + ], ]; } /** * @dataProvider dataValidate - * @param string $regex * @param string[] $expectedTypes - * @param bool $expectedHasAnchors - * @param bool $expectAllErrorsIgnored */ public function testValidate( string $regex, array $expectedTypes, bool $expectedHasAnchors, - bool $expectAllErrorsIgnored + bool $expectAllErrorsIgnored, ): void { - $grammar = new \Hoa\File\Read('hoa://Library/Regex/Grammar.pp'); - $parser = \Hoa\Compiler\Llk\Llk::load($grammar); + $grammar = new Read(__DIR__ . '/../../../resources/RegexGrammar.pp'); + $parser = Llk::load($grammar); $validator = new IgnoredRegexValidator($parser, self::getContainer()->getByType(TypeStringResolver::class)); $result = $validator->validate($regex); 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 @@ files()->name('composer.json')->in(__DIR__ . '/../../../vendor') as $fileInfo) { $realpath = $fileInfo->getRealPath(); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $json = Json::decode(FileReader::read($realpath), Json::FORCE_ARRAY); if (!isset($json['autoload']['files'])) { @@ -34,7 +43,10 @@ public function testExpectedFiles(): void } foreach ($json['autoload']['files'] as $file) { - $autoloadFile = substr(dirname($realpath) . '/' . $file, strlen($vendorPath) + 1); + $autoloadFile = substr(dirname($realpath) . DIRECTORY_SEPARATOR . $file, strlen($vendorPath) + 1); + if (strpos($autoloadFile, 'rector' . DIRECTORY_SEPARATOR . 'rector' . DIRECTORY_SEPARATOR) === 0) { + continue; + } $autoloadFiles[] = $fileHelper->normalizePath($autoloadFile); } } @@ -44,19 +56,22 @@ public function testExpectedFiles(): void $expectedFiles = [ 'hoa/consistency/Prelude.php', // Hoa isn't prefixed, no need to load this eagerly 'hoa/protocol/Wrapper.php', // Hoa isn't prefixed, no need to load this eagerly - 'jetbrains/phpstorm-stubs/PhpStormStubsMap.php', // added to bin/phpstan + 'jetbrains/phpstorm-stubs/PhpStormStubsMap.php', // added to phpstan-dist/bootstrap.php 'myclabs/deep-copy/src/DeepCopy/deep_copy.php', // dev dependency of PHPUnit - 'react/promise-timer/src/functions_include.php', // added to bin/phpstan - 'react/promise/src/functions_include.php', // added to bin/phpstan + 'react/async/src/functions_include.php', // added to phpstan-dist/bootstrap.php + 'react/promise/src/functions_include.php', // added to phpstan-dist/bootstrap.php + 'phpunit/phpunit/src/Framework/Assert/Functions.php', + 'symfony/deprecation-contracts/function.php', // afaik polyfills aren't necessary 'symfony/polyfill-ctype/bootstrap.php', // afaik polyfills aren't necessary + 'symfony/polyfill-intl-grapheme/bootstrap.php', // afaik polyfills aren't necessary + 'symfony/polyfill-intl-normalizer/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-mbstring/bootstrap.php', // afaik polyfills aren't necessary - 'symfony/polyfill-php73/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-php80/bootstrap.php', // afaik polyfills aren't necessary + 'symfony/polyfill-php81/bootstrap.php', // afaik polyfills aren't necessary + 'symfony/string/Resources/functions.php', // afaik polyfills aren't necessary ]; - $expectedFiles = array_map(static function (string $path) use ($fileHelper): string { - return $fileHelper->normalizePath($path); - }, $expectedFiles); + $expectedFiles = array_map(static fn (string $path): string => $fileHelper->normalizePath($path), $expectedFiles); sort($expectedFiles); $this->assertSame($expectedFiles, $autoloadFiles); diff --git a/tests/PHPStan/Dependency/DependencyDumperTest.php b/tests/PHPStan/Dependency/DependencyDumperTest.php deleted file mode 100644 index c0568b6f7a..0000000000 --- a/tests/PHPStan/Dependency/DependencyDumperTest.php +++ /dev/null @@ -1,144 +0,0 @@ -getByType(NodeScopeResolver::class); - - /** @var Parser $realParser */ - $realParser = $container->getByType(Parser::class); - - $mockParser = $this->createMock(Parser::class); - $mockParser->method('parseFile') - ->willReturnCallback(static function (string $file) use ($realParser): array { - if (file_exists($file)) { - return $realParser->parseFile($file); - } - - return []; - }); - - /** @var Broker $realBroker */ - $realBroker = $container->getByType(Broker::class); - - $fileHelper = new FileHelper(__DIR__); - - $mockBroker = $this->createMock(Broker::class); - $mockBroker->method('getClass') - ->willReturnCallback(function (string $class) use ($realBroker, $fileHelper): ClassReflection { - if (in_array($class, [ - GrandChild::class, - Child::class, - ParentClass::class, - ], true)) { - return $realBroker->getClass($class); - } - - $nameParts = explode('\\', $class); - $shortClass = array_pop($nameParts); - - $classReflection = $this->createMock(ClassReflection::class); - $classReflection->method('getInterfaces')->willReturn([]); - $classReflection->method('getTraits')->willReturn([]); - $classReflection->method('getParentClass')->willReturn(false); - $classReflection->method('getFilename')->willReturn( - $fileHelper->normalizePath(__DIR__ . '/data/' . $shortClass . '.php') - ); - - return $classReflection; - }); - - $expectedDependencyTree = $this->getExpectedDependencyTree($fileHelper); - - /** @var ScopeFactory $scopeFactory */ - $scopeFactory = $container->getByType(ScopeFactory::class); - - /** @var FileFinder $fileFinder */ - $fileFinder = $container->getByType(FileFinder::class); - - $dumper = new DependencyDumper( - new DependencyResolver($mockBroker), - $nodeScopeResolver, - $fileHelper, - $mockParser, - $scopeFactory, - $fileFinder - ); - - $dependencies = $dumper->dumpDependencies( - array_merge( - [$fileHelper->normalizePath(__DIR__ . '/data/GrandChild.php')], - array_keys($expectedDependencyTree) - ), - static function (): void { - }, - static function (): void { - }, - null - ); - - $this->assertCount(count($expectedDependencyTree), $dependencies); - foreach ($expectedDependencyTree as $file => $files) { - $this->assertArrayHasKey($file, $dependencies); - $this->assertSame($files, $dependencies[$file]); - } - } - - /** - * @param FileHelper $fileHelper - * @return string[][] - */ - private function getExpectedDependencyTree(FileHelper $fileHelper): array - { - $tree = [ - 'Child.php' => [ - 'GrandChild.php', - ], - 'Parent.php' => [ - 'GrandChild.php', - 'Child.php', - ], - 'MethodNativeReturnTypehint.php' => [ - 'GrandChild.php', - ], - 'MethodPhpDocReturnTypehint.php' => [ - 'GrandChild.php', - ], - 'ParamNativeReturnTypehint.php' => [ - 'GrandChild.php', - ], - 'ParamPhpDocReturnTypehint.php' => [ - 'GrandChild.php', - ], - ]; - - $expectedTree = []; - foreach ($tree as $file => $files) { - $expectedTree[$fileHelper->normalizePath(__DIR__ . '/data/' . $file)] = array_map(static function (string $file) use ($fileHelper): string { - return $fileHelper->normalizePath(__DIR__ . '/data/' . $file); - }, $files); - } - - return $expectedTree; - } - -} diff --git a/tests/PHPStan/Dependency/data/Child.php b/tests/PHPStan/Dependency/data/Child.php deleted file mode 100644 index 8e9e8a3495..0000000000 --- a/tests/PHPStan/Dependency/data/Child.php +++ /dev/null @@ -1,8 +0,0 @@ -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, ], ]; @@ -117,19 +117,17 @@ public function dataExcludeOnWindows(): array /** * @dataProvider dataExcludeOnUnix - * @param string $filePath * @param string[] $analyseExcludes - * @param bool $isExcluded */ public function testFilesAreExcludedFromAnalysingOnUnix( string $filePath, array $analyseExcludes, - bool $isExcluded + bool $isExcluded, ): void { $this->skipIfNotOnUnix(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, []); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } @@ -144,7 +142,7 @@ public function dataExcludeOnUnix(): array ], [ __DIR__ . '/data/excluded-file.php', - [__DIR__], + [__DIR__ . '/*'], true, ], [ @@ -172,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'], @@ -194,7 +187,7 @@ public function dataExcludeOnUnix(): array ], [ '/etc/phpstan/dummy-1.php', - ['/etc/phpstan/'], + ['/etc/phpstan/*'], true, ], [ @@ -204,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 9e2d88ec8d..409936a8d1 100644 --- a/tests/PHPStan/File/FileHelperTest.php +++ b/tests/PHPStan/File/FileHelperTest.php @@ -2,7 +2,9 @@ namespace PHPStan\File; -class FileHelperTest extends \PHPStan\Testing\TestCase +use PHPStan\Testing\PHPStanTestCase; + +class FileHelperTest extends PHPStanTestCase { /** @@ -18,13 +20,13 @@ 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'], ]; } /** * @dataProvider dataAbsolutizePathOnWindows - * @param string $path - * @param string $absolutePath */ public function testAbsolutizePathOnWindows(string $path, string $absolutePath): void { @@ -47,13 +49,13 @@ 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'], ]; } /** * @dataProvider dataAbsolutizePathOnLinuxOrMac - * @param string $path - * @param string $absolutePath */ public function testAbsolutizePathOnLinuxOrMac(string $path, string $absolutePath): void { @@ -75,13 +77,12 @@ 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'], ]; } /** * @dataProvider dataNormalizePathOnWindows - * @param string $path - * @param string $normalizedPath */ public function testNormalizePathOnWindows(string $path, string $normalizedPath): void { @@ -102,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'], ]; @@ -109,8 +111,6 @@ public function dataNormalizePathOnLinuxOrMac(): array /** * @dataProvider dataNormalizePathOnLinuxOrMac - * @param string $path - * @param string $normalizedPath */ public function testNormalizePathOnLinuxOrMac(string $path, string $normalizedPath): void { diff --git a/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php b/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php index bd0cb74822..d4065efb28 100644 --- a/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php +++ b/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php @@ -105,20 +105,17 @@ public function dataGetRelativePath(): array /** * @dataProvider dataGetRelativePath - * @param string $parentDirectory - * @param string $filename - * @param string $expectedRelativePath */ public function testGetRelativePath( string $parentDirectory, string $filename, - string $expectedRelativePath + string $expectedRelativePath, ): void { $helper = new ParentDirectoryRelativePathHelper($parentDirectory); $this->assertSame( $expectedRelativePath, - $helper->getRelativePath($filename) + $helper->getRelativePath($filename), ); } diff --git a/tests/PHPStan/File/RelativePathHelperTest.php b/tests/PHPStan/File/RelativePathHelperTest.php index 7dd10064eb..5d5b3eca41 100644 --- a/tests/PHPStan/File/RelativePathHelperTest.php +++ b/tests/PHPStan/File/RelativePathHelperTest.php @@ -2,7 +2,12 @@ namespace PHPStan\File; -class RelativePathHelperTest extends \PHPUnit\Framework\TestCase +use PHPUnit\Framework\TestCase; +use function array_map; +use function str_replace; +use function substr; + +class RelativePathHelperTest extends TestCase { public function dataGetRelativePath(): array @@ -137,42 +142,70 @@ 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', + ], ]; } /** * @dataProvider dataGetRelativePath - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ public function testGetRelativePathOnUnix( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { - $helper = new FuzzyRelativePathHelper($currentWorkingDirectory, $analysedPaths, '/'); + $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $currentWorkingDirectory, $analysedPaths, '/'); $this->assertSame( $expectedResult, - $helper->getRelativePath($filenameToRelativize) + $helper->getRelativePath($filenameToRelativize), ); } /** * @dataProvider dataGetRelativePath - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ public function testGetRelativePathOnWindows( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $sanitize = static function (string $path): string { @@ -182,10 +215,10 @@ public function testGetRelativePathOnWindows( return str_replace('/', '\\', $path); }; - $helper = new FuzzyRelativePathHelper($sanitize($currentWorkingDirectory), array_map($sanitize, $analysedPaths), '\\'); + $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $sanitize($currentWorkingDirectory), array_map($sanitize, $analysedPaths), '\\'); $this->assertSame( $sanitize($expectedResult), - $helper->getRelativePath($sanitize($filenameToRelativize)) + $helper->getRelativePath($sanitize($filenameToRelativize)), ); } @@ -215,22 +248,19 @@ public function dataGetRelativePathWindowsSpecific(): array /** * @dataProvider dataGetRelativePathWindowsSpecific - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ public function testGetRelativePathWindowsSpecific( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { - $helper = new FuzzyRelativePathHelper($currentWorkingDirectory, $analysedPaths, '\\'); + $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $currentWorkingDirectory, $analysedPaths, '\\'); $this->assertSame( $expectedResult, - $helper->getRelativePath($filenameToRelativize) + $helper->getRelativePath($filenameToRelativize), ); } 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/AnotherTestEnum.php b/tests/PHPStan/Fixture/AnotherTestEnum.php new file mode 100644 index 0000000000..0a6352ae4f --- /dev/null +++ b/tests/PHPStan/Fixture/AnotherTestEnum.php @@ -0,0 +1,12 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum AnotherTestEnum: int +{ + + case ONE = 1; + case TWO = 2; + const CONST_ONE = 1; + +} diff --git a/tests/PHPStan/Fixture/FinalClass.php b/tests/PHPStan/Fixture/FinalClass.php new file mode 100644 index 0000000000..43d05f45ee --- /dev/null +++ b/tests/PHPStan/Fixture/FinalClass.php @@ -0,0 +1,8 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum ManyCasesTestEnum +{ + + case A; + case B; + case C; + case D; + case E; + case F; + +} diff --git a/tests/PHPStan/Fixture/TestEnum.php b/tests/PHPStan/Fixture/TestEnum.php new file mode 100644 index 0000000000..dad59d83b9 --- /dev/null +++ b/tests/PHPStan/Fixture/TestEnum.php @@ -0,0 +1,12 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum TestEnum: int implements TestEnumInterface +{ + + case ONE = 1; + case TWO = 2; + const CONST_ONE = 1; + +} diff --git a/tests/PHPStan/Fixture/TestEnumInterface.php b/tests/PHPStan/Fixture/TestEnumInterface.php new file mode 100644 index 0000000000..c26b4d28d3 --- /dev/null +++ b/tests/PHPStan/Fixture/TestEnumInterface.php @@ -0,0 +1,8 @@ += 8.1 + +namespace PHPStan\Fixture; + +interface TestEnumInterface +{ + +} diff --git a/tests/PHPStan/Generics/GenericsIntegrationTest.php b/tests/PHPStan/Generics/GenericsIntegrationTest.php index c637d3fd3a..a100809355 100644 --- a/tests/PHPStan/Generics/GenericsIntegrationTest.php +++ b/tests/PHPStan/Generics/GenericsIntegrationTest.php @@ -2,13 +2,15 @@ namespace PHPStan\Generics; +use PHPStan\Testing\LevelsTestCase; + /** - * @group exec + * @group levels */ -class GenericsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class GenericsIntegrationTest extends LevelsTestCase { - public function dataTopics(): array + public static function dataTopics(): array { return [ ['functions'], @@ -17,11 +19,13 @@ public function dataTopics(): array ['varyingAcceptor'], ['classes'], ['variance'], + ['typeProjections'], ['bug2574'], ['bug2577'], ['bug2620'], ['bug2622'], ['bug2627'], + ['bug6210'], ]; } diff --git a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php index 57af50c613..16fe1d24df 100644 --- a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php +++ b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php @@ -2,8 +2,8 @@ namespace PHPStan\Generics; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -14,8 +14,9 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class TemplateTypeFactoryTest extends \PHPStan\Testing\TestCase +class TemplateTypeFactoryTest extends PHPStanTestCase { /** @return array */ @@ -36,7 +37,11 @@ public function dataCreate(): array ], [ new StringType(), - new MixedType(), + new StringType(), + ], + [ + new IntegerType(), + new IntegerType(), ], [ new ErrorType(), @@ -47,7 +52,7 @@ public function dataCreate(): array TemplateTypeScope::createWithFunction('a'), 'U', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -56,7 +61,10 @@ public function dataCreate(): array new StringType(), new IntegerType(), ]), - new MixedType(), + new UnionType([ + new StringType(), + new IntegerType(), + ]), ], ]; } @@ -71,13 +79,12 @@ public function testCreate(?Type $bound, Type $expectedBound): void $scope, 'T', $bound, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); - $this->assertInstanceOf(TemplateType::class, $templateType); $this->assertTrue( $expectedBound->equals($templateType->getBound()), - sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())) + sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Generics/data/Bug2574.php b/tests/PHPStan/Generics/data/Bug2574.php deleted file mode 100644 index 1c761095d6..0000000000 --- a/tests/PHPStan/Generics/data/Bug2574.php +++ /dev/null @@ -1,19 +0,0 @@ -newInstance(); -} 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/bug6210.php b/tests/PHPStan/Generics/data/bug6210.php new file mode 100644 index 0000000000..5a4202fd03 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug6210.php @@ -0,0 +1,29 @@ +show($entity); + } + + /** + * @phpstan-param TEntityClass $entity + */ + private function show($entity): void + { + } +} diff --git a/tests/PHPStan/Generics/data/classes-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/classes-3.json b/tests/PHPStan/Generics/data/classes-7.json similarity index 100% rename from tests/PHPStan/Generics/data/classes-3.json rename to tests/PHPStan/Generics/data/classes-7.json diff --git a/tests/PHPStan/Generics/data/functions-6.json b/tests/PHPStan/Generics/data/functions-6.json index 00b36cab69..72e5278a64 100644 --- a/tests/PHPStan/Generics/data/functions-6.json +++ b/tests/PHPStan/Generics/data/functions-6.json @@ -1,6 +1,6 @@ [ { - "message": "Function PHPStan\\Generics\\Functions\\testF() has no return typehint specified.", + "message": "Function PHPStan\\Generics\\Functions\\testF() has no return type specified.", "line": 27, "ignorable": true }, diff --git a/tests/PHPStan/Generics/data/invalidReturn-3.json b/tests/PHPStan/Generics/data/invalidReturn-7.json similarity index 100% rename from tests/PHPStan/Generics/data/invalidReturn-3.json rename to tests/PHPStan/Generics/data/invalidReturn-7.json diff --git a/tests/PHPStan/Generics/data/pick-6.json b/tests/PHPStan/Generics/data/pick-6.json index 4b5aef0bd0..1598be5320 100644 --- a/tests/PHPStan/Generics/data/pick-6.json +++ b/tests/PHPStan/Generics/data/pick-6.json @@ -1,6 +1,6 @@ [ { - "message": "Function PHPStan\\Generics\\Pick\\test() has no return typehint specified.", + "message": "Function PHPStan\\Generics\\Pick\\test() has no return type specified.", "line": 22, "ignorable": true } diff --git a/tests/PHPStan/Generics/data/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-0.json b/tests/PHPStan/Generics/data/variance-0.json new file mode 100644 index 0000000000..9b097eebd1 --- /dev/null +++ b/tests/PHPStan/Generics/data/variance-0.json @@ -0,0 +1,12 @@ +[ + { + "message": "Template type T of function PHPStan\\Generics\\Variance\\returnOut() is not referenced in a parameter.", + "line": 109, + "ignorable": true + }, + { + "message": "Template type T of function PHPStan\\Generics\\Variance\\returnInvariant() is not referenced in a parameter.", + "line": 117, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-2.json b/tests/PHPStan/Generics/data/variance-2.json index dd610744ca..3c7d9da4d4 100644 --- a/tests/PHPStan/Generics/data/variance-2.json +++ b/tests/PHPStan/Generics/data/variance-2.json @@ -40,32 +40,17 @@ "ignorable": true }, { - "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter a of function PHPStan\\Generics\\Variance\\x().", + "message": "Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type T in in function PHPStan\\Generics\\Variance\\x().", "line": 101, "ignorable": true }, { - "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter c of function PHPStan\\Generics\\Variance\\x().", - "line": 101, - "ignorable": true - }, - { - "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter d of function PHPStan\\Generics\\Variance\\x().", - "line": 101, - "ignorable": true - }, - { - "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter e of function PHPStan\\Generics\\Variance\\x().", - "line": 101, - "ignorable": true - }, - { - "message": "Template type T is declared as covariant, but occurs in invariant position in parameter b of function PHPStan\\Generics\\Variance\\x().", - "line": 101, + "message": "Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type T in in function PHPStan\\Generics\\Variance\\returnOut().", + "line": 109, "ignorable": true }, { - "message": "Template type T is declared as covariant, but occurs in invariant position in return type of function PHPStan\\Generics\\Variance\\returnInvariant().", + "message": "Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type T in in function PHPStan\\Generics\\Variance\\returnInvariant().", "line": 117, "ignorable": true }, @@ -75,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 new file mode 100644 index 0000000000..7757cc3dea --- /dev/null +++ b/tests/PHPStan/Generics/data/variance-4.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property PHPStan\\Generics\\Variance\\ConstructorAndStatic::$data is never read, only written.", + "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/Generics/data/varyingAcceptor-5.json b/tests/PHPStan/Generics/data/varyingAcceptor-5.json index 3b41536df4..5445d01cf6 100644 --- a/tests/PHPStan/Generics/data/varyingAcceptor-5.json +++ b/tests/PHPStan/Generics/data/varyingAcceptor-5.json @@ -8,5 +8,10 @@ "message": "Parameter #2 $t1 of function PHPStan\\Generics\\VaryingAcceptor\\applyReversed expects callable(PHPStan\\Generics\\VaryingAcceptor\\A): void, Closure(PHPStan\\Generics\\VaryingAcceptor\\B): void given.", "line": 42, "ignorable": true + }, + { + "message": "Parameter #1 $closure of function PHPStan\\Generics\\VaryingAcceptor\\bar expects callable(callable(): string): string, callable(callable(): int): string given.", + "line": 55, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/varyingAcceptor-6.json b/tests/PHPStan/Generics/data/varyingAcceptor-6.json index cb26b5d815..72f7b7dc7a 100644 --- a/tests/PHPStan/Generics/data/varyingAcceptor-6.json +++ b/tests/PHPStan/Generics/data/varyingAcceptor-6.json @@ -1,6 +1,6 @@ [ { - "message": "Function PHPStan\\Generics\\VaryingAcceptor\\testApply() has no return typehint specified.", + "message": "Function PHPStan\\Generics\\VaryingAcceptor\\testApply() has no return type specified.", "line": 30, "ignorable": true } diff --git a/tests/PHPStan/Generics/data/varyingAcceptor-7.json b/tests/PHPStan/Generics/data/varyingAcceptor-7.json deleted file mode 100644 index 06eab487be..0000000000 --- a/tests/PHPStan/Generics/data/varyingAcceptor-7.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Parameter #1 $closure of function PHPStan\\Generics\\VaryingAcceptor\\bar expects callable(callable(): int|string): int|string, callable(callable(): int): string given.", - "line": 55, - "ignorable": true - } -] \ No newline at end of file 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 c3dacc310b..d8d3c28878 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -2,13 +2,15 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** - * @group exec + * @group levels */ -class InferPrivatePropertyTypeFromConstructorIntegrationTest extends \PHPStan\Testing\LevelsTestCase +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 0230eddb6b..0000000000 --- a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php +++ /dev/null @@ -1,38 +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 new file mode 100644 index 0000000000..d35d7dade7 --- /dev/null +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -0,0 +1,40 @@ + callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 579, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array() given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{} given.", "line": 580, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('foo' => 1) given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 1} given.", "line": 582, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('foo' => 'nonexistent') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 'nonexistent'} given.", "line": 584, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('bar' => 'date') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{bar: 'date'} given.", "line": 585, "ignorable": true }, + { + "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, @@ -198,5 +173,20 @@ "message": "Parameter #1 $min (340) of function random_int expects lower number than parameter #2 $max (int).", "line": 685, "ignorable": true + }, + { + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, 'foo' given.", + "line": 707, + "ignorable": true + }, + { + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", + "line": 733, + "ignorable": true + }, + { + "message": "Parameter #2 $array of function implode expects array|null, int given.", + "line": 763, + "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/acceptTypes-6.json b/tests/PHPStan/Levels/data/acceptTypes-6.json index 543bf6620a..35bd142bb9 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-6.json +++ b/tests/PHPStan/Levels/data/acceptTypes-6.json @@ -1,152 +1,167 @@ [ { - "message": "Method Levels\\AcceptTypes\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doFoo() has no return type specified.", "line": 15, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBar() has no return type specified.", "line": 30, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBaz() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBaz() has no return type specified.", "line": 41, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doLorem() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doLorem() has no return type specified.", "line": 60, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doIpsum() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doIpsum() has no return type specified.", "line": 68, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doFooArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doFooArray() has no return type specified.", "line": 80, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBarArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBarArray() has no return type specified.", "line": 98, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBazArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBazArray() has no return type specified.", "line": 103, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBazArrayUnionItemTypes() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBazArrayUnionItemTypes() has no return type specified.", "line": 127, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::callableArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::callableArray() has no return type specified.", "line": 138, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::expectCallable() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::expectCallable() has no return type specified.", "line": 148, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::iterableCountable() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::iterableCountable() has no return type specified.", "line": 160, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::benevolentUnionNotReported() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::benevolentUnionNotReported() has no return type specified.", "line": 176, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFoo() has no return type specified.", "line": 208, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFooUnionClosures() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFooUnionClosures() has no return type specified.", "line": 254, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBar() has no return type specified.", "line": 332, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBaz() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBaz() has no return type specified.", "line": 342, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doFoo() has no return type specified.", "line": 409, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doBar() has no return type specified.", "line": 418, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doBaz() has no return type specified.", "line": 423, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doLorem() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doLorem() has no return type specified.", "line": 437, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doIpsum() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doIpsum() has no return type specified.", "line": 445, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doFooArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doFooArray() has no return type specified.", "line": 490, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doBarArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doBarArray() has no return type specified.", "line": 502, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::testUnions() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::testUnions() has no return type specified.", "line": 524, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::testUnions2() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::testUnions2() has no return type specified.", "line": 535, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::requireArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::requireArray() has no return type specified.", "line": 549, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::requireFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::requireFoo() has no return type specified.", "line": 554, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ArrayShapes::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ArrayShapes::doFoo() has no return type specified.", "line": 570, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ArrayShapes::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ArrayShapes::doBar() has no return type specified.", "line": 603, "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has no return type specified.", + "line": 755, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has parameter $union with no value type specified in iterable type array.", + "line": 755, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::invalidType() has no return type specified.", + "line": 762, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 3c2ec409ca..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, @@ -35,10 +30,40 @@ "ignorable": true }, { - "message": "Parameter #1 $var of function count expects array|Countable, iterable given.", + "message": "Parameter #1 $value of function count expects array|Countable, iterable given.", "line": 167, "ignorable": true }, + { + "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)|(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\\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\\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, @@ -60,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 }, @@ -70,32 +95,32 @@ "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 }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 577, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 578, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array()|array('foo' => 'date') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{}|array{foo: 'date'} given.", "line": 596, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), iterable given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, iterable given.", "line": 597, "ignorable": true }, @@ -114,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, @@ -133,5 +153,20 @@ "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", "line": 692, "ignorable": true + }, + { + "message": "Parameter #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 a90e72c311..61b2c1fbbb 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -205,7 +205,7 @@ class FooImpl implements FooInterface class ClosureAccepts { - public function doFoo() + public function doFoo(ParentFooInterface $parent) { $c = function (FooInterface $x, $y): FooInterface { return new FooImpl(); @@ -244,17 +244,17 @@ public function doFoo() $this->doBar($c); $this->doBaz($c); - $c = function (FooInterface $x): ParentFooInterface { // return type contravariance - error - return new FooImpl(); + $c = function (FooInterface $x) use ($parent): ParentFooInterface { // return type contravariance - error + return $parent; }; $this->doBar($c); $this->doBaz($c); } - public function doFooUnionClosures() + public function doFooUnionClosures(FooInterface $foo, ParentFooInterface $parent) { - $closure = function (): FooInterface { - return new FooImpl(); + $closure = function () use ($foo): FooInterface { + return $foo; }; $c = function (FooInterface $x, $y): FooInterface { return new FooImpl(); @@ -310,8 +310,8 @@ public function doFooUnionClosures() $this->doBar($c); $this->doBaz($c); - $c = function (FooInterface $x): ParentFooInterface { // return type contravariance - error - return new FooImpl(); + $c = function (FooInterface $x) use ($parent): ParentFooInterface { // return type contravariance - error + return $parent; }; if (rand(0, 1) === 0) { $c = $closure; @@ -319,8 +319,8 @@ public function doFooUnionClosures() $this->doBar($c); $this->doBaz($c); - $c = function () { - + $c = function ($mixed) { + return $mixed; }; $this->doBar($c); $this->doBaz($c); @@ -693,3 +693,89 @@ public function doStuff(): void } } + +class NumericStrings +{ + + /** + * @param string $string + * @param numeric-string $numericString + */ + public function doFoo(string $string, string $numericString): void + { + $this->doBar('1'); + $this->doBar('foo'); + $this->doBar($string); + $this->doBar($numericString); + } + + /** + * @param numeric-string $numericString + */ + public function doBar(string $numericString): void + { + + } +} + +class AcceptNonEmpty +{ + + /** + * @param array $array + * @param non-empty-array $nonEmpty + */ + public function doFoo( + array $array, + array $nonEmpty + ): void + { + $this->doBar([]); + $this->doBar([1, 2, 3]); + $this->doBar($array); + $this->doBar($nonEmpty); + } + + /** + * @param non-empty-array $nonEmpty + */ + public function doBar( + array $nonEmpty + ): void + { + + } + +} + +class Implode { + /** + * @param string|int|array $union + */ + public function partlySupportedUnion($union) { + $imploded = implode('abc', $union); + } + + /** + * @param int $invalid + */ + public function invalidType($invalid) { + $imploded = implode('abc', $invalid); + } +} + +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/arrayAccess-3.json b/tests/PHPStan/Levels/data/arrayAccess-3.json index 56d5badd36..dcc1810c46 100644 --- a/tests/PHPStan/Levels/data/arrayAccess-3.json +++ b/tests/PHPStan/Levels/data/arrayAccess-3.json @@ -1,7 +1,7 @@ [ { - "message": "Cannot assign offset int to SplObjectStorage.", + "message": "Cannot assign offset int to SplObjectStorage.", "line": 35, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayAccess-6.json b/tests/PHPStan/Levels/data/arrayAccess-6.json index bf65c24f00..3a8d649bcb 100644 --- a/tests/PHPStan/Levels/data/arrayAccess-6.json +++ b/tests/PHPStan/Levels/data/arrayAccess-6.json @@ -1,26 +1,26 @@ [ { - "message": "Method Levels\\ArrayAccess\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doFoo() has no return type specified.", "line": 11, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doBar() has no return type specified.", "line": 22, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doBaz() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doBaz() has no return type specified.", "line": 30, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has no return type specified.", "line": 38, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has parameter $mixed with no typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has parameter $mixed with no type specified.", "line": 38, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/arrayAccess-7.json b/tests/PHPStan/Levels/data/arrayAccess-7.json index f5eb3a4552..64c7a328fb 100644 --- a/tests/PHPStan/Levels/data/arrayAccess-7.json +++ b/tests/PHPStan/Levels/data/arrayAccess-7.json @@ -1,7 +1,7 @@ [ { - "message": "Cannot assign offset int|object to SplObjectStorage.", + "message": "Cannot assign offset int|object to SplObjectStorage.", "line": 27, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-3.json b/tests/PHPStan/Levels/data/arrayDestructuring-3.json new file mode 100644 index 0000000000..a97c71f0f5 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDestructuring-3.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot use array destructuring on iterable.", + "line": 23, + "ignorable": true + }, + { + "message": "Offset 3 does not exist on array{'a', 'b', 'c'}.", + "line": 30, + "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 new file mode 100644 index 0000000000..3b033abd6d --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot use array destructuring on array|null.", + "line": 15, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayDestructuring.php b/tests/PHPStan/Levels/data/arrayDestructuring.php new file mode 100644 index 0000000000..c97a3e6328 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDestructuring.php @@ -0,0 +1,33 @@ + $it + */ + public function doBar(iterable $it): void + { + [$a] = $it; + } + + public function doBaz(): void + { + $array = ['a', 'b', 'c']; + [$a] = $array; + [$a, , , $d] = $array; + } + +} 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-3.json b/tests/PHPStan/Levels/data/arrayDimFetches-3.json index 04ef5e889c..73c0481c0e 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-3.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-3.json @@ -1,17 +1,7 @@ [ { - "message": "Cannot access offset 1 on stdClass.", - "line": 14, - "ignorable": true - }, - { - "message": "Offset 'b' does not exist on array('a' => 1).", + "message": "Offset 'b' does not exist on array{a: 1}.", "line": 21, "ignorable": true - }, - { - "message": "Offset 'b' does not exist on array('a' => 1)|stdClass.", - "line": 28, - "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-6.json b/tests/PHPStan/Levels/data/arrayDimFetches-6.json index dfc3b07751..1a39a16f63 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-6.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-6.json @@ -1,11 +1,11 @@ [ { - "message": "Method Levels\\ArrayDimFetches\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ArrayDimFetches\\Foo::doFoo() has no return type specified.", "line": 12, "ignorable": true }, { - "message": "Method Levels\\ArrayDimFetches\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\ArrayDimFetches\\Foo::doBar() has no return type specified.", "line": 31, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7-missing.json b/tests/PHPStan/Levels/data/arrayDimFetches-7-missing.json deleted file mode 100644 index b7c2d4ac49..0000000000 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7-missing.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Offset 'b' does not exist on array('a' => 1)|stdClass.", - "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 132611f89f..23df32943a 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -1,16 +1,21 @@ [ { - "message": "Cannot access offset 'a' on array('a' => 1)|stdClass.", + "message": "Cannot access offset 1 on stdClass.", + "line": 14, + "ignorable": true + }, + { + "message": "Cannot access offset 'a' on array{a: 1}|stdClass.", "line": 27, "ignorable": true }, { - "message": "Cannot access offset 'b' on array('a' => 1)|stdClass.", + "message": "Cannot access offset 'b' on array{a: 1}|stdClass.", "line": 28, "ignorable": true }, { - "message": "Offset 'b' does not exist on array('a' => 1, ?'b' => 1).", + "message": "Offset 'b' might not exist on array{a: 1, b?: 1}.", "line": 40, "ignorable": true }, @@ -23,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-missing.json b/tests/PHPStan/Levels/data/arrayDimFetches-8-missing.json deleted file mode 100644 index b7c2d4ac49..0000000000 --- a/tests/PHPStan/Levels/data/arrayDimFetches-8-missing.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Offset 'b' does not exist on array('a' => 1)|stdClass.", - "line": 28, - "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-9.json b/tests/PHPStan/Levels/data/arrayDimFetches-9.json new file mode 100644 index 0000000000..fb1695ce25 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDimFetches-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 15, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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-6.json b/tests/PHPStan/Levels/data/binaryOps-6.json index 41ae5dc405..f71cae89e6 100644 --- a/tests/PHPStan/Levels/data/binaryOps-6.json +++ b/tests/PHPStan/Levels/data/binaryOps-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\BinaryOps\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\BinaryOps\\Foo::doFoo() has no return type specified.", "line": 14, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/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-6.json b/tests/PHPStan/Levels/data/callableCalls-6.json index 1ac782fa92..ad0d233ac4 100644 --- a/tests/PHPStan/Levels/data/callableCalls-6.json +++ b/tests/PHPStan/Levels/data/callableCalls-6.json @@ -1,11 +1,11 @@ [ { - "message": "Method Levels\\CallableCalls\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\CallableCalls\\Foo::doFoo() has no return type specified.", "line": 14, "ignorable": true }, { - "message": "Method Levels\\CallableCalls\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\CallableCalls\\Foo::doBar() has no return type specified.", "line": 41, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/callableCalls-8.json b/tests/PHPStan/Levels/data/callableCalls-8.json index 8c5a7c589f..3d8f385589 100644 --- a/tests/PHPStan/Levels/data/callableCalls-8.json +++ b/tests/PHPStan/Levels/data/callableCalls-8.json @@ -1,11 +1,11 @@ [ { - "message": "Trying to invoke (Closure(int): mixed)|null but it might not be a callable.", + "message": "Trying to invoke (Closure(int): void)|null but it might not be a callable.", "line": 37, "ignorable": true }, { - "message": "Trying to invoke (Closure(int): mixed)|null but it might not be a callable.", + "message": "Trying to invoke (Closure(int): void)|null but it might not be a callable.", "line": 38, "ignorable": true }, diff --git a/tests/PHPStan/Levels/data/callableCalls-9-missing.json b/tests/PHPStan/Levels/data/callableCalls-9-missing.json new file mode 100644 index 0000000000..5c7f12b38d --- /dev/null +++ b/tests/PHPStan/Levels/data/callableCalls-9-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Closure invoked with 0 parameters, 1 required.", + "line": 37, + "ignorable": true + }, + { + "message": "Trying to invoke int but it's not a callable.", + "line": 43, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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/callableVariance-6.json b/tests/PHPStan/Levels/data/callableVariance-6.json index d5b814feae..28eec65b64 100644 --- a/tests/PHPStan/Levels/data/callableVariance-6.json +++ b/tests/PHPStan/Levels/data/callableVariance-6.json @@ -1,11 +1,11 @@ [ { - "message": "Function Levels\\CallableVariance\\d() has no return typehint specified.", + "message": "Function Levels\\CallableVariance\\d() has no return type specified.", "line": 68, "ignorable": true }, { - "message": "Function Levels\\CallableVariance\\testD() has no return typehint specified.", + "message": "Function Levels\\CallableVariance\\testD() has no return type specified.", "line": 79, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/callableVariance.php b/tests/PHPStan/Levels/data/callableVariance.php index ec6552995e..3156d9bcb6 100644 --- a/tests/PHPStan/Levels/data/callableVariance.php +++ b/tests/PHPStan/Levels/data/callableVariance.php @@ -53,9 +53,9 @@ function c(callable $cb): void */ function testC($a, $b, $c): void { - c(function (): A { throw new \Exception(); }); - c(function (): B { throw new \Exception(); }); - c(function (): C { throw new \Exception(); }); + c(function (): A { return new A(); }); + c(function (): B { return new B(); }); + c(function (): C { return new C(); }); c($a); c($b); diff --git a/tests/PHPStan/Levels/data/casts-2.json b/tests/PHPStan/Levels/data/casts-2.json deleted file mode 100644 index 3fef9a1842..0000000000 --- a/tests/PHPStan/Levels/data/casts-2.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "message": "Cannot cast array to int.", - "line": 19, - "ignorable": true - }, - { - "message": "Cannot cast array|(callable(): mixed) to int.", - "line": 20, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/casts-6.json b/tests/PHPStan/Levels/data/casts-6.json index 842590fde6..1fc1590698 100644 --- a/tests/PHPStan/Levels/data/casts-6.json +++ b/tests/PHPStan/Levels/data/casts-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Casts\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Casts\\Foo::doFoo() has no return type specified.", "line": 13, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/casts-7.json b/tests/PHPStan/Levels/data/casts-7.json index 40dd8a8de5..e2810b0e42 100644 --- a/tests/PHPStan/Levels/data/casts-7.json +++ b/tests/PHPStan/Levels/data/casts-7.json @@ -1,7 +1,12 @@ [ { - "message": "Cannot cast array|float|int to string.", + "message": "Cannot cast array|(callable(): mixed) to int.", + "line": 20, + "ignorable": true + }, + { + "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/casts.php b/tests/PHPStan/Levels/data/casts.php index f81df52c07..67cf2dacd8 100644 --- a/tests/PHPStan/Levels/data/casts.php +++ b/tests/PHPStan/Levels/data/casts.php @@ -7,7 +7,7 @@ class Foo /** * @param mixed[] $array - * @param mixed[]|callable $arrayOrCallable + * @param mixed[]|(callable(): mixed) $arrayOrCallable * @param mixed[]|float|int $arrayOrFloatOrInt */ public function doFoo( 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-6.json b/tests/PHPStan/Levels/data/clone-6.json index 20f2c4389a..5a448fefc5 100644 --- a/tests/PHPStan/Levels/data/clone-6.json +++ b/tests/PHPStan/Levels/data/clone-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Cloning\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Cloning\\Foo::doFoo() has no return type specified.", "line": 18, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/clone-9-missing.json b/tests/PHPStan/Levels/data/clone-9-missing.json new file mode 100644 index 0000000000..40e1203120 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-9-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot clone non-object variable $nullableInt of type int.", + "line": 34, + "ignorable": true + }, + { + "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-9.json b/tests/PHPStan/Levels/data/clone-9.json new file mode 100644 index 0000000000..8148322a42 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot clone non-object variable $mixed of type mixed.", + "line": 36, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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-0.json b/tests/PHPStan/Levels/data/coalesce-0.json deleted file mode 100644 index d4d84323dd..0000000000 --- a/tests/PHPStan/Levels/data/coalesce-0.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Undefined variable: $bar", - "line": 6, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-1.json b/tests/PHPStan/Levels/data/coalesce-1.json index 6636fbc672..f329d4e78d 100644 --- a/tests/PHPStan/Levels/data/coalesce-1.json +++ b/tests/PHPStan/Levels/data/coalesce-1.json @@ -8,5 +8,15 @@ "message": "Variable $bar on left side of ?? is never defined.", "line": 6, "ignorable": true + }, + { + "message": "Variable $a on left side of ?? is never defined.", + "line": 15, + "ignorable": true + }, + { + "message": "Variable $s on left side of ?? always exists and is always null.", + "line": 23, + "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/coalesce-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/coalesce.php b/tests/PHPStan/Levels/data/coalesce.php index 6010637b0a..fffbf433ca 100644 --- a/tests/PHPStan/Levels/data/coalesce.php +++ b/tests/PHPStan/Levels/data/coalesce.php @@ -10,3 +10,15 @@ function (\ReflectionClass $ref): void { echo $ref->name ?? 'foo'; echo $ref->nonexistent ?? 'bar'; }; + +function (?string $s): void { + echo $a ?? 'foo'; + + echo $s ?? 'bar'; + + if ($s !== null) { + return; + } + + echo $s ?? 'bar'; +}; diff --git a/tests/PHPStan/Levels/data/comparison-6.json b/tests/PHPStan/Levels/data/comparison-6.json index 39a509dbc4..c470766a2e 100644 --- a/tests/PHPStan/Levels/data/comparison-6.json +++ b/tests/PHPStan/Levels/data/comparison-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Comparison\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Comparison\\Foo::doFoo() has no return type specified.", "line": 18, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/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/constantAccesses-6.json b/tests/PHPStan/Levels/data/constantAccesses-6.json index 8c3f1d74ee..2a9e2775c7 100644 --- a/tests/PHPStan/Levels/data/constantAccesses-6.json +++ b/tests/PHPStan/Levels/data/constantAccesses-6.json @@ -1,11 +1,11 @@ [ { - "message": "Method Levels\\ConstantAccesses\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ConstantAccesses\\Foo::doFoo() has no return type specified.", "line": 14, "ignorable": true }, { - "message": "Method Levels\\ConstantAccesses\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\ConstantAccesses\\Baz::doBaz() has no return type specified.", "line": 42, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/constantAccesses-9-missing.json b/tests/PHPStan/Levels/data/constantAccesses-9-missing.json new file mode 100644 index 0000000000..0cc5a3f5d4 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses-9-missing.json @@ -0,0 +1,17 @@ +[ + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Foo::BAR_CONSTANT.", + "line": 53, + "ignorable": true + }, + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Bar|Levels\\ConstantAccesses\\Foo::BAR_CONSTANT.", + "line": 56, + "ignorable": true + }, + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Bar|Levels\\ConstantAccesses\\Foo::FOO_CONSTANT.", + "line": 55, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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/echo_.php b/tests/PHPStan/Levels/data/echo_.php index de7f934dbc..ad6925d276 100644 --- a/tests/PHPStan/Levels/data/echo_.php +++ b/tests/PHPStan/Levels/data/echo_.php @@ -8,7 +8,7 @@ class Foo { /** * @param mixed[] $array - * @param mixed[]|callable $arrayOrCallable + * @param mixed[]|(callable(): mixed) $arrayOrCallable * @param mixed[]|float|int $arrayOrFloatOrInt * @param mixed[]|string $arrayOrString */ 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/encapsedString.php b/tests/PHPStan/Levels/data/encapsedString.php index 08876acd58..910076d41e 100644 --- a/tests/PHPStan/Levels/data/encapsedString.php +++ b/tests/PHPStan/Levels/data/encapsedString.php @@ -7,7 +7,7 @@ class Foo /** * @param mixed[] $array - * @param mixed[]|callable $arrayOrCallable + * @param mixed[]|(callable(): mixed) $arrayOrCallable * @param mixed[]|float|int $arrayOrFloatOrInt * @param mixed[]|string $arrayOrString */ diff --git a/tests/PHPStan/Levels/data/inferPropertyType-6.json b/tests/PHPStan/Levels/data/inferPropertyType-6.json index 20a2ad3c6e..42ce35fafd 100644 --- a/tests/PHPStan/Levels/data/inferPropertyType-6.json +++ b/tests/PHPStan/Levels/data/inferPropertyType-6.json @@ -1,11 +1,11 @@ [ { - "message": "Property InferPropertyType\\Foo::$bar has no typehint specified.", + "message": "Property InferPropertyType\\Foo::$bar has no type specified.", "line": 10, "ignorable": true }, { - "message": "Method InferPropertyType\\Foo::doFoo() has no return typehint specified.", + "message": "Method InferPropertyType\\Foo::doFoo() has no return type specified.", "line": 18, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/iterable-6.json b/tests/PHPStan/Levels/data/iterable-6.json index d2cccc8bcb..d77f788ce1 100644 --- a/tests/PHPStan/Levels/data/iterable-6.json +++ b/tests/PHPStan/Levels/data/iterable-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Iterables\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Iterables\\Foo::doFoo() has no return type specified.", "line": 15, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/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-0.json b/tests/PHPStan/Levels/data/methodCalls-0.json index 658d0d8ec2..44ceee285a 100644 --- a/tests/PHPStan/Levels/data/methodCalls-0.json +++ b/tests/PHPStan/Levels/data/methodCalls-0.json @@ -16,7 +16,7 @@ }, { "message": "Method Levels\\MethodCalls\\ExtraArguments::doFoo() invoked with 0 parameters, 1 required.", - "line": 231, + "line": 236, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/methodCalls-1.json b/tests/PHPStan/Levels/data/methodCalls-1.json index 14a7e678ff..e10054e781 100644 --- a/tests/PHPStan/Levels/data/methodCalls-1.json +++ b/tests/PHPStan/Levels/data/methodCalls-1.json @@ -11,7 +11,7 @@ }, { "message": "Method Levels\\MethodCalls\\ExtraArguments::doFoo() invoked with 2 parameters, 1 required.", - "line": 233, + "line": 238, "ignorable": true } -] \ No newline at end of file +] 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/methodCalls-6.json b/tests/PHPStan/Levels/data/methodCalls-6.json index 9fc32bd6d9..60f6147b69 100644 --- a/tests/PHPStan/Levels/data/methodCalls-6.json +++ b/tests/PHPStan/Levels/data/methodCalls-6.json @@ -1,72 +1,72 @@ [ { - "message": "Method Levels\\MethodCalls\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Foo::doFoo() has no return type specified.", "line": 8, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\Bar::doBar() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Bar::doBar() has no return type specified.", "line": 23, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Baz::doBaz() has no return type specified.", "line": 45, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::doFoo() has no return type specified.", "line": 70, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::__call() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::__call() has no return type specified.", "line": 79, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 89, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::__callStatic() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::__callStatic() has no return type specified.", "line": 98, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\Ipsum::doLorem() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Ipsum::doLorem() has no return type specified.", "line": 158, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\FooException::commonMethod() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\FooException::commonMethod() has no return type specified.", "line": 182, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\FooException::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\FooException::doFoo() has no return type specified.", "line": 187, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\BarException::commonMethod() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\BarException::commonMethod() has no return type specified.", "line": 197, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\BarException::doBar() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\BarException::doBar() has no return type specified.", "line": 202, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\TestExceptions::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\TestExceptions::doFoo() has no return type specified.", "line": 212, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\ExtraArguments::doFoo() has no return typehint specified.", - "line": 229, + "message": "Method Levels\\MethodCalls\\ExtraArguments::doFoo() has no return type specified.", + "line": 234, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/methodCalls-9-missing.json b/tests/PHPStan/Levels/data/methodCalls-9-missing.json new file mode 100644 index 0000000000..47cdcab769 --- /dev/null +++ b/tests/PHPStan/Levels/data/methodCalls-9-missing.json @@ -0,0 +1,52 @@ +[ + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 53, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 56, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 59, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 162, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 166, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 170, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 59, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 60, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 170, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 171, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/methodCalls.php b/tests/PHPStan/Levels/data/methodCalls.php index fa92b0849a..06cb63f6ff 100644 --- a/tests/PHPStan/Levels/data/methodCalls.php +++ b/tests/PHPStan/Levels/data/methodCalls.php @@ -212,7 +212,7 @@ class TestExceptions public function doFoo() { try { - + $this->doBar(); } catch (FooException | BarException $e) { $e->commonMethod(); $e->doFoo(); @@ -221,6 +221,11 @@ public function doFoo() } } + public function doBar(): void + { + + } + } class ExtraArguments diff --git a/tests/PHPStan/Levels/data/missingReturn-0.json b/tests/PHPStan/Levels/data/missingReturn-0.json index d11943ca76..98754e893f 100644 --- a/tests/PHPStan/Levels/data/missingReturn-0.json +++ b/tests/PHPStan/Levels/data/missingReturn-0.json @@ -2,6 +2,11 @@ { "message": "Method Levels\\MissingReturn\\Foo::doFoo() should return int but return statement is missing.", "line": 8, + "ignorable": false + }, + { + "message": "Method Levels\\MissingReturn\\Foo::doBar() should return int but return statement is missing.", + "line": 16, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/missingReturn-2.json b/tests/PHPStan/Levels/data/missingReturn-2.json deleted file mode 100644 index 3a4788f45d..0000000000 --- a/tests/PHPStan/Levels/data/missingReturn-2.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Method Levels\\MissingReturn\\Foo::doBar() should return int but return statement is missing.", - "line": 16, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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 @@ += 8.0 + +namespace NamedArgumentsIntegrationTest; + +class Foo +{ + + public function doFoo( + int $i, + int $j, + int $k, ?int $l = null + ): void + { + + } + + public function doBar(): void + { + $this->doFoo( + i: 1, + 2, + 3 + ); + $this->doFoo( + 1, + j: 3 + ); + $this->doFoo( + 'foo', + j: 3, + k: 4 + ); + $this->doFoo( + i: 'foo', + j: 3, + k: 4 + ); + $this->doFoo(i: 1, ...['j' => 2, 'k' => 3]); + $this->doFoo(...['k' => 3, 'i' => 1, 'j' => 'str']); + $this->doFoo(...['k' => 3, 'i' => 1, 'str']); + } + + public function doBaz(self $self): void + { + $self->doFoo( + i: 1, + 2, + 3 + ); + $self->doFoo( + 1, + j: 3 + ); + $self->doFoo( + 'foo', + j: 3, + k: 4 + ); + $self->doFoo( + i: 'foo', + j: 3, + k: 4 + ); + $self->doFoo(i: 1, ...['j' => 2, 'k' => 3]); + $self->doFoo(...['k' => 3, 'i' => 1, 'j' => 'str']); + $self->doFoo(...['k' => 3, 'i' => 1, 'str']); + } + +} diff --git a/tests/PHPStan/Levels/data/object-10-missing.json b/tests/PHPStan/Levels/data/object-10-missing.json new file mode 100644 index 0000000000..4d1f2153ba --- /dev/null +++ b/tests/PHPStan/Levels/data/object-10-missing.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to an undefined method object::foo().", + "line": 25, + "ignorable": true + }, + { + "message": "Access to an undefined property object::$bar.", + "line": 26, + "ignorable": true + }, + { + "message": "Call to an undefined static method object::baz().", + "line": 28, + "ignorable": true + }, + { + "message": "Access to an undefined static property object::$dolor.", + "line": 29, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/object-10.json b/tests/PHPStan/Levels/data/object-10.json new file mode 100644 index 0000000000..57f727da24 --- /dev/null +++ b/tests/PHPStan/Levels/data/object-10.json @@ -0,0 +1,32 @@ +[ + { + "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": 17, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 26, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 29, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 38, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 41, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/object-6.json b/tests/PHPStan/Levels/data/object-6.json index f98cc39107..3110d6eed2 100644 --- a/tests/PHPStan/Levels/data/object-6.json +++ b/tests/PHPStan/Levels/data/object-6.json @@ -1,16 +1,16 @@ [ { - "message": "Method Levels\\ObjectTests\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ObjectTests\\Foo::doFoo() has no return type specified.", "line": 11, "ignorable": true }, { - "message": "Method Levels\\ObjectTests\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\ObjectTests\\Foo::doBar() has no return type specified.", "line": 23, "ignorable": true }, { - "message": "Method Levels\\ObjectTests\\Foo::doBaz() has no return typehint specified.", + "message": "Method Levels\\ObjectTests\\Foo::doBaz() has no return type specified.", "line": 35, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/object-9-missing.json b/tests/PHPStan/Levels/data/object-9-missing.json new file mode 100644 index 0000000000..4d1f2153ba --- /dev/null +++ b/tests/PHPStan/Levels/data/object-9-missing.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to an undefined method object::foo().", + "line": 25, + "ignorable": true + }, + { + "message": "Access to an undefined property object::$bar.", + "line": 26, + "ignorable": true + }, + { + "message": "Call to an undefined static method object::baz().", + "line": 28, + "ignorable": true + }, + { + "message": "Access to an undefined static property object::$dolor.", + "line": 29, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/print_-2.json b/tests/PHPStan/Levels/data/print_-2.json index d0a53d8362..5978bf90e0 100644 --- a/tests/PHPStan/Levels/data/print_-2.json +++ b/tests/PHPStan/Levels/data/print_-2.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter array of print cannot be converted to string.", + "message": "Parameter array 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/print_.php b/tests/PHPStan/Levels/data/print_.php index 0a8bf29f76..ba2fe5265e 100644 --- a/tests/PHPStan/Levels/data/print_.php +++ b/tests/PHPStan/Levels/data/print_.php @@ -8,7 +8,7 @@ class Foo { /** * @param mixed[] $array - * @param mixed[]|callable $arrayOrCallable + * @param mixed[]|(callable(): mixed) $arrayOrCallable * @param mixed[]|float|int $arrayOrFloatOrInt * @param mixed[]|string $arrayOrString */ 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 f0d47cd16e..b62abeefd8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-6.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-6.json @@ -1,42 +1,37 @@ [ { - "message": "Method Levels\\PropertyAccesses\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Foo::doFoo() has no return type specified.", "line": 11, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\Bar::doBar() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Bar::doBar() has no return type specified.", "line": 29, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Baz::doBaz() has no return type specified.", "line": 50, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::doFoo() has no return type specified.", "line": 74, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::__set() has no return typehint specified.", - "line": 83, - "ignorable": true - }, - { - "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 93, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::__get() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::__get() has no return type specified.", "line": 98, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\Ipsum::doBaz() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Ipsum::doBaz() has no return type specified.", "line": 158, "ignorable": true } -] \ 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 new file mode 100644 index 0000000000..fd6f669c7c --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-9-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-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/returnTypes-2.json b/tests/PHPStan/Levels/data/returnTypes-0.json similarity index 100% rename from tests/PHPStan/Levels/data/returnTypes-2.json rename to tests/PHPStan/Levels/data/returnTypes-0.json diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-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-9.json b/tests/PHPStan/Levels/data/stringOffsetAccess-9.json new file mode 100644 index 0000000000..1e218ab052 --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-9.json @@ -0,0 +1,42 @@ +[ + { + "message": "Cannot access offset 0 on mixed.", + "line": 39, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 39, + "ignorable": true + }, + { + "message": "Cannot access offset 'foo' on mixed.", + "line": 43, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 43, + "ignorable": true + }, + { + "message": "Cannot access offset 12.34 on mixed.", + "line": 47, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 47, + "ignorable": true + }, + { + "message": "Cannot access offset int|object on mixed.", + "line": 51, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 51, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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 0625085298..ce13d6e997 100644 --- a/tests/PHPStan/Levels/data/stubValidator-0.json +++ b/tests/PHPStan/Levels/data/stubValidator-0.json @@ -1,32 +1,32 @@ [ { - "message": "Method StubValidator\\Foo::doFoo() has no return typehint specified.", + "message": "Method StubValidator\\Foo::doFoo() has no return type specified.", "line": 15, - "ignorable": true + "ignorable": false }, { "message": "Method StubValidator\\Foo::doFoo() has parameter $argument with no value type specified in iterable type array.", "line": 15, - "ignorable": true + "ignorable": false }, { - "message": "Function StubValidator\\someFunction() has no return typehint specified.", + "message": "Function StubValidator\\someFunction() has no return type specified.", "line": 22, - "ignorable": true + "ignorable": false }, { "message": "Function StubValidator\\someFunction() has parameter $argument with no value type specified in iterable type array.", "line": 22, - "ignorable": true + "ignorable": false }, { - "message": "Method class@anonymous/stubValidator/stubs.php:27::doFoo() has no return typehint specified.", + "message": "Method ArrayIterator@anonymous/stubValidator/stubs.php:27::doFoo() has no return type specified.", "line": 30, - "ignorable": true + "ignorable": false }, { - "message": "Parameter $foo of method class@anonymous/stubValidator/stubs.php:27::doFoo() has invalid typehint type StubValidator\\Foooooooo.", + "message": "Parameter $foo of method ArrayIterator@anonymous/stubValidator/stubs.php:27::doFoo() has invalid type StubValidator\\Foooooooo.", "line": 30, - "ignorable": true + "ignorable": false } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/stubs-functions-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-functions-6.json b/tests/PHPStan/Levels/data/stubs-functions-6.json index 2b701606cf..68b139c505 100644 --- a/tests/PHPStan/Levels/data/stubs-functions-6.json +++ b/tests/PHPStan/Levels/data/stubs-functions-6.json @@ -1,11 +1,11 @@ [ { - "message": "Function StubsIntegrationTest\\foo() has no return typehint specified.", + "message": "Function StubsIntegrationTest\\foo() has no return type specified.", "line": 5, "ignorable": true }, { - "message": "Function StubsIntegrationTest\\foo() has parameter $i with no typehint specified.", + "message": "Function StubsIntegrationTest\\foo() has parameter $i with no type specified.", "line": 5, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/stubs-methods-3.json b/tests/PHPStan/Levels/data/stubs-methods-3.json index 1c0402a627..9f8132c6e9 100644 --- a/tests/PHPStan/Levels/data/stubs-methods-3.json +++ b/tests/PHPStan/Levels/data/stubs-methods-3.json @@ -23,10 +23,5 @@ "message": "Anonymous function should return int but returns string.", "line": 128, "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetYetAnotherFoo::doFoo() should return stdClass but returns string.", - "line": 219, - "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stubs-methods-4.json b/tests/PHPStan/Levels/data/stubs-methods-4.json index ab8a491ae3..eb0d8a3325 100644 --- a/tests/PHPStan/Levels/data/stubs-methods-4.json +++ b/tests/PHPStan/Levels/data/stubs-methods-4.json @@ -1,36 +1,36 @@ [ { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 47, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 58, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 89, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 108, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 144, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 158, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 175, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/stubs-methods-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-6.json b/tests/PHPStan/Levels/data/stubs-methods-6.json deleted file mode 100644 index 9b15e1a786..0000000000 --- a/tests/PHPStan/Levels/data/stubs-methods-6.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "message": "Method StubsIntegrationTest\\Foo::doFoo() has no return typehint specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\Foo::doFoo() has parameter $i with no typehint specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\InterfaceWithStubPhpDoc2::doFoo() has no return typehint specified.", - "line": 151, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetAnotherFoo::doFoo() has no return typehint specified.", - "line": 197, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetAnotherFoo::doFoo() has parameter $j with no typehint specified.", - "line": 197, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/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/throwValues-6.json b/tests/PHPStan/Levels/data/throwValues-6.json index ae6f775eef..ccbd412f3b 100644 --- a/tests/PHPStan/Levels/data/throwValues-6.json +++ b/tests/PHPStan/Levels/data/throwValues-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\ThrowValues\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ThrowValues\\Foo::doFoo() has no return type specified.", "line": 30, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/typehints-0.json b/tests/PHPStan/Levels/data/typehints-0.json index 07084c7e86..46421c021f 100644 --- a/tests/PHPStan/Levels/data/typehints-0.json +++ b/tests/PHPStan/Levels/data/typehints-0.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doFoo() has invalid typehint type Levels\\Typehints\\Lorem.", + "message": "Method Levels\\Typehints\\Foo::doFoo() has invalid return type Levels\\Typehints\\Ipsum.", "line": 8, "ignorable": true }, { - "message": "Return typehint of method Levels\\Typehints\\Foo::doFoo() has invalid type Levels\\Typehints\\Ipsum.", + "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doFoo() has invalid type Levels\\Typehints\\Lorem.", "line": 8, "ignorable": true }, diff --git a/tests/PHPStan/Levels/data/typehints-2.json b/tests/PHPStan/Levels/data/typehints-2.json index adbf371a07..1f1a947306 100644 --- a/tests/PHPStan/Levels/data/typehints-2.json +++ b/tests/PHPStan/Levels/data/typehints-2.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doBar() has invalid typehint type Levels\\Typehints\\Lorem.", + "message": "Method Levels\\Typehints\\Foo::doBar() has invalid return type Levels\\Typehints\\Ipsum.", "line": 17, "ignorable": true }, { - "message": "Return typehint of method Levels\\Typehints\\Foo::doBar() has invalid type Levels\\Typehints\\Ipsum.", + "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doBar() has invalid type Levels\\Typehints\\Lorem.", "line": 17, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/unreachable-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 05edefa8b4..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 typehint specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return typehint specified.", - "line": 18, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return typehint specified.", - "line": 27, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return typehint specified.", - "line": 36, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return typehint specified.", - "line": 45, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return typehint specified.", - "line": 54, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return typehint specified.", - "line": 71, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return typehint specified.", - "line": 77, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return typehint specified.", - "line": 82, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return typehint specified.", - "line": 87, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return typehint specified.", - "line": 92, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return typehint specified.", - "line": 97, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-6.json b/tests/PHPStan/Levels/data/unreachable-6.json index 05edefa8b4..b285fc696d 100644 --- a/tests/PHPStan/Levels/data/unreachable-6.json +++ b/tests/PHPStan/Levels/data/unreachable-6.json @@ -1,61 +1,61 @@ [ { - "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return type specified.", "line": 8, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return type specified.", "line": 18, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return type specified.", "line": 27, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return type specified.", "line": 36, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return type specified.", "line": 45, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return type specified.", "line": 54, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return type specified.", "line": 71, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return type specified.", "line": 77, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return type specified.", "line": 82, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return type specified.", "line": 87, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return type specified.", "line": 92, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return type specified.", "line": 97, "ignorable": true } diff --git a/tests/PHPStan/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/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 07faa2dc3e..60a132a7f8 100644 --- a/tests/PHPStan/Node/FileNodeTest.php +++ b/tests/PHPStan/Node/FileNodeTest.php @@ -5,9 +5,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Testing\RuleTestCase; +use function get_class; +use function sprintf; +use const DIRECTORY_SEPARATOR; class FileNodeTest extends RuleTestCase { @@ -22,9 +26,8 @@ public function getNodeType(): string } /** - * @param \PHPStan\Node\FileNode $node - * @param \PHPStan\Analyser\Scope $scope - * @return \PHPStan\Rules\RuleError[] + * @param FileNode $node + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -32,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(), + sprintf('First node in file %s is: %s', $pathHelper->getRelativePath($scope->getFile()), get_class($nodes[0])), + )->identifier('tests.fileNode')->build(), ]; } @@ -69,9 +74,6 @@ public function dataRule(): iterable /** * @dataProvider dataRule - * @param string $file - * @param string $expectedError - * @param int $line */ public function testRule(string $file, string $expectedError, int $line): void { diff --git a/tests/PHPStan/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/Node/data/parent-stmt-types.php b/tests/PHPStan/Node/data/parent-stmt-types.php new file mode 100644 index 0000000000..745d46855c --- /dev/null +++ b/tests/PHPStan/Node/data/parent-stmt-types.php @@ -0,0 +1,16 @@ + escapeshellarg($path), [ __DIR__ . '/data/trait-definition.php', __DIR__ . '/data/traits.php', - ])) + ])), ), $outputLines, $exitCode); $output = implode("\n", $outputLines); + FileSystem::delete($tmpDir); + $fileHelper = new FileHelper(__DIR__); $filePath = $fileHelper->normalizePath(__DIR__ . '/data/trait-definition.php'); $this->assertJsonStringEqualsJsonString(Json::encode([ 'totals' => [ 'errors' => 0, - 'file_errors' => 3, + 'file_errors' => 4, ], 'files' => [ sprintf('%s (in context of class ParallelAnalyserIntegrationTest\\Bar)', $filePath) => [ 'errors' => 1, 'messages' => [ [ - 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return typehint specified.', + 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], ], ], sprintf('%s (in context of class ParallelAnalyserIntegrationTest\\Foo)', $filePath) => [ - 'errors' => 2, + 'errors' => 3, 'messages' => [ [ - 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return typehint specified.', + 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + '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 adf59fcf4e..fb1fd626cf 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -3,6 +3,9 @@ namespace PHPStan\Parallel; use PHPUnit\Framework\TestCase; +use function array_fill; +use function array_map; +use function count; class SchedulerTest extends TestCase { @@ -55,17 +58,24 @@ public function dataSchedule(): array 2, [30, 30, 30, 30, 4], ], + [ + 16, + 16, + 2, + 20, + 1, + 1, + [1], + ], ]; } /** * @dataProvider dataSchedule - * @param int $cpuCores - * @param int $maximumNumberOfProcesses - * @param int $minimumNumberOfJobsPerProcess - * @param int $jobSize - * @param int $numberOfFiles - * @param int $expectedNumberOfProcesses + * @param positive-int $jobSize + * @param positive-int $maximumNumberOfProcesses + * @param positive-int $minimumNumberOfJobsPerProcess + * @param 0|positive-int $numberOfFiles * @param array $expectedJobSizes */ public function testSchedule( @@ -75,7 +85,7 @@ public function testSchedule( int $jobSize, int $numberOfFiles, int $expectedNumberOfProcesses, - array $expectedJobSizes + array $expectedJobSizes, ): void { $files = array_fill(0, $numberOfFiles, 'file.php'); @@ -83,9 +93,7 @@ public function testSchedule( $schedule = $scheduler->scheduleWork($cpuCores, $files); $this->assertSame($expectedNumberOfProcesses, $schedule->getNumberOfProcesses()); - $jobSizes = array_map(static function (array $job): int { - return count($job); - }, $schedule->getJobs()); + $jobSizes = array_map(static fn (array $job): int => count($job), $schedule->getJobs()); $this->assertSame($expectedJobSizes, $jobSizes); } diff --git a/tests/PHPStan/Parallel/data/trait-definition.php b/tests/PHPStan/Parallel/data/trait-definition.php index edf01e73f8..499e4040f7 100644 --- a/tests/PHPStan/Parallel/data/trait-definition.php +++ b/tests/PHPStan/Parallel/data/trait-definition.php @@ -10,4 +10,9 @@ public function doFoo() $this->test = 1; } + public function getFoo(): int + { + return $this->test; + } + } diff --git a/tests/PHPStan/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 ca71124e28..3b97a1b99a 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -2,52 +2,33 @@ namespace PHPStan\Parser; -class CachedParserTest extends \PHPUnit\Framework\TestCase +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 $cachedNodesByFileCountMax - * @param int $cachedNodesByStringCountMax - * @param int $cachedNodesByFileCountExpected - * @param int $cachedNodesByStringCountExpected */ public function testParseFileClearCache( - int $cachedNodesByFileCountMax, int $cachedNodesByStringCountMax, - int $cachedNodesByFileCountExpected, - int $cachedNodesByStringCountExpected + int $cachedNodesByStringCountExpected, ): void { $parser = new CachedParser( $this->getParserMock(), - $cachedNodesByFileCountMax, - $cachedNodesByStringCountMax - ); - - $this->assertEquals( - $cachedNodesByFileCountMax, - $parser->getCachedNodesByFileCountMax() - ); - - $this->assertEquals( $cachedNodesByStringCountMax, - $parser->getCachedNodesByStringCountMax() ); - // Add files to cache - for ($i = 0; $i <= $cachedNodesByFileCountMax; $i++) { - $parser->parseFile('file' . $i); - } - $this->assertEquals( - $cachedNodesByFileCountExpected, - $parser->getCachedNodesByFileCount() - ); - - $this->assertCount( - $cachedNodesByFileCountExpected, - $parser->getCachedNodesByFile() + $cachedNodesByStringCountMax, + $parser->getCachedNodesByStringCountMax(), ); // Add strings to cache @@ -57,36 +38,29 @@ 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' => [ - 'cachedNodesByFileCountMax' => 100, 'cachedNodesByStringCountMax' => 50, - 'cachedNodesByFileCountExpected' => 100, 'cachedNodesByStringCountExpected' => 50, ]; yield 'odd' => [ - 'cachedNodesByFileCountMax' => 101, 'cachedNodesByStringCountMax' => 51, - 'cachedNodesByFileCountExpected' => 101, 'cachedNodesByStringCountExpected' => 51, ]; } - /** - * @return Parser&\PHPUnit\Framework\MockObject\MockObject - */ - private function getParserMock(): Parser + private function getParserMock(): Parser&MockObject { $mock = $this->createMock(Parser::class); @@ -96,12 +70,54 @@ 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 + { + $fileHelper = self::getContainer()->getByType(FileHelper::class); + $pathRoutingParser = new PathRoutingParser( + $fileHelper, + self::getContainer()->getService('currentPhpVersionRichParser'), + self::getContainer()->getService('currentPhpVersionSimpleDirectParser'), + self::getContainer()->getService('php8Parser'), + ); + $parser = new CachedParser($pathRoutingParser, 500); + $path = $fileHelper->normalizePath(__DIR__ . '/data/test.php'); + $pathRoutingParser->setAnalysedFiles([$path]); + $contents = FileReader::read($path); + $stmts = $parser->parseString($contents); + $this->assertInstanceOf(Namespace_::class, $stmts[0]); + $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(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(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 new file mode 100644 index 0000000000..69c0e8af10 --- /dev/null +++ b/tests/PHPStan/Parser/CleaningParserTest.php @@ -0,0 +1,86 @@ +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 new file mode 100644 index 0000000000..1d22a6ac92 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-1-after.php @@ -0,0 +1,51 @@ += 80100) { + doFoo1(); + doFoo2(); +} else { + doBar1(); + doBar2(); +} diff --git a/tests/PHPStan/Parser/data/cleaning-php-version-before2.php b/tests/PHPStan/Parser/data/cleaning-php-version-before2.php new file mode 100644 index 0000000000..4060f97c45 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-php-version-before2.php @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..b7ee628d37 --- /dev/null +++ b/tests/PHPStan/Parser/data/test.php @@ -0,0 +1,5 @@ += 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 @@ +create(); + $this->assertSame($expectedVersion, $phpVersion->getVersionId()); + + if ($expectedVersionString === null) { + return; + } + + $this->assertSame($expectedVersionString, $phpVersion->getVersionString()); + } + +} diff --git a/tests/PHPStan/Php/PhpVersionsTest.php b/tests/PHPStan/Php/PhpVersionsTest.php new file mode 100644 index 0000000000..b1563e2eb1 --- /dev/null +++ b/tests/PHPStan/Php/PhpVersionsTest.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/PhpDoc/TypeDescriptionTest.php b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php new file mode 100644 index 0000000000..29c3a35231 --- /dev/null +++ b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php @@ -0,0 +1,98 @@ +', new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType()])]; + yield ['class-string&literal-string', new IntersectionType([new ClassStringType(), new AccessoryLiteralStringType()])]; + yield ['class-string&literal-string', new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()])]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType()); + yield ['array{foo: int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType(), true); + yield ['array{foo?: int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType(), true); + $builder->setOffsetValueType(new ConstantStringType('bar'), new StringType()); + yield ['array{foo?: int, bar: string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(null, new IntegerType()); + $builder->setOffsetValueType(null, new StringType()); + yield ['array{int, string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(null, new IntegerType()); + $builder->setOffsetValueType(null, new StringType(), true); + yield ['array{0: int, 1?: string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('\'foo\''), new IntegerType()); + yield ['array{"\'foo\'": int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('"foo"'), new IntegerType()); + yield ['array{\'"foo"\': int}', $builder->getArray()]; + } + + /** + * @dataProvider dataTest + */ + public function testParsingDesiredTypeDescription(string $description, Type $expectedType): void + { + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($description); + $this->assertTrue($expectedType->equals($type), sprintf('Parsing %s did not result in %s, but in %s', $description, $expectedType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))); + + $newDescription = $type->describe(VerbosityLevel::value()); + $newType = $typeStringResolver->resolve($newDescription); + $this->assertTrue($type->equals($newType), sprintf('Parsing %s again did not result in %s, but in %s', $newDescription, $type->describe(VerbosityLevel::value()), $newType->describe(VerbosityLevel::value()))); + } + + /** + * @dataProvider dataTest + */ + public function testDesiredTypeDescription(string $description, Type $expectedType): void + { + $this->assertSame($description, $expectedType->describe(VerbosityLevel::value())); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($description); + $this->assertSame($description, $type->describe(VerbosityLevel::value())); + } + +} diff --git a/tests/PHPStan/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 d585540774..36b4402d9e 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -2,21 +2,28 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsMethods\Bar; +use AnnotationsMethods\Baz; +use AnnotationsMethods\BazBaz; +use AnnotationsMethods\Foo; +use AnnotationsMethods\FooInterface; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function count; +use function sprintf; -class AnnotationsMethodsClassReflectionExtensionTest extends \PHPStan\Testing\TestCase +class AnnotationsMethodsClassReflectionExtensionTest extends PHPStanTestCase { public function dataMethods(): array { $fooMethods = [ 'getInteger' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, @@ -38,7 +45,7 @@ public function dataMethods(): array ], ], 'doSomething' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -60,21 +67,21 @@ public function dataMethods(): array ], ], 'getFooOrBar' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnType' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, @@ -96,7 +103,7 @@ public function dataMethods(): array ], ], 'doSomethingStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -118,21 +125,21 @@ public function dataMethods(): array ], ], 'getFooOrBarStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, @@ -154,7 +161,7 @@ public function dataMethods(): array ], ], 'doSomethingWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -176,21 +183,21 @@ public function dataMethods(): array ], ], 'getFooOrBarWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, @@ -212,7 +219,7 @@ public function dataMethods(): array ], ], 'doSomethingStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -234,154 +241,154 @@ public function dataMethods(): array ], ], 'getFooOrBarStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClass' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'bool', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'bool|string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'float|string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodFromInterface' => [ - 'class' => \AnnotationsMethods\FooInterface::class, - 'returnType' => \AnnotationsMethods\FooInterface::class, + 'class' => FooInterface::class, + 'returnType' => FooInterface::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'publish' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'Aws\Result', 'isStatic' => false, 'isVariadic' => false, @@ -396,7 +403,7 @@ public function dataMethods(): array ], ], 'rotate' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Image', 'isStatic' => false, 'isVariadic' => false, @@ -418,15 +425,15 @@ public function dataMethods(): array ], ], 'overridenMethod' => [ - 'class' => \AnnotationsMethods\Foo::class, - 'returnType' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'overridenMethodWithAnnotation' => [ - 'class' => \AnnotationsMethods\Foo::class, - 'returnType' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], @@ -436,33 +443,33 @@ public function dataMethods(): array $fooMethods, [ 'overridenMethod' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'overridenMethodWithAnnotation' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'conflictingMethod' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], - ] + ], ); $bazMethods = array_merge( $barMethods, [ 'doSomething' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -484,7 +491,7 @@ public function dataMethods(): array ], ], 'getIpsum' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => false, 'isVariadic' => false, @@ -499,7 +506,7 @@ public function dataMethods(): array ], ], 'getIpsumStatically' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => true, 'isVariadic' => false, @@ -514,7 +521,7 @@ public function dataMethods(): array ], ], 'getIpsumWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => false, 'isVariadic' => false, @@ -529,7 +536,7 @@ public function dataMethods(): array ], ], 'getIpsumStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => true, 'isVariadic' => false, @@ -544,7 +551,7 @@ public function dataMethods(): array ], ], 'doSomethingStatically' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -566,7 +573,7 @@ public function dataMethods(): array ], ], 'doSomethingWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -588,7 +595,7 @@ public function dataMethods(): array ], ], 'doSomethingStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -610,75 +617,75 @@ public function dataMethods(): array ], ], 'doSomethingNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodFromTrait' => [ - 'class' => \AnnotationsMethods\Baz::class, - 'returnType' => \AnnotationsMethods\BazBaz::class, + 'class' => Baz::class, + 'returnType' => BazBaz::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], - ] + ], ); $bazBazMethods = array_merge( $bazMethods, [ 'getTest' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getTestStatically' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getTestWithDescription' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getTestStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithSpecificScalarParamsWithoutDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -714,7 +721,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificScalarParamsWithDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -750,7 +757,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificObjectParamsWithoutDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -786,7 +793,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificObjectParamsWithDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -822,7 +829,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicScalarParamsNotNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -837,7 +844,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicScalarParamsNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -852,7 +859,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicObjectParamsNotNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -867,7 +874,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicObjectParamsNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -882,7 +889,7 @@ public function dataMethods(): array ], ], 'doSomethingWithComplicatedParameters' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -918,7 +925,7 @@ public function dataMethods(): array ], ], 'paramMultipleTypesWithExtraSpaces' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'float|int', 'isStatic' => false, 'isVariadic' => false, @@ -939,27 +946,25 @@ public function dataMethods(): array ], ], ], - ] + ], ); return [ - [\AnnotationsMethods\Foo::class, $fooMethods], - [\AnnotationsMethods\Bar::class, $barMethods], - [\AnnotationsMethods\Baz::class, $bazMethods], - [\AnnotationsMethods\BazBaz::class, $bazBazMethods], + [Foo::class, $fooMethods], + [Bar::class, $barMethods], + [Baz::class, $bazMethods], + [BazBaz::class, $bazBazMethods], ]; } /** * @dataProvider dataMethods - * @param string $className * @param array $methods */ public function testMethods(string $className, array $methods): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); @@ -968,51 +973,51 @@ 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(), - sprintf('Declaring class of method %s() does not match.', $methodName) + sprintf('Declaring class of method %s() does not match.', $methodName), ); $this->assertSame( $expectedMethodData['returnType'], $selectedParametersAcceptor->getReturnType()->describe(VerbosityLevel::precise()), - sprintf('Return type of method %s::%s() does not match', $className, $methodName) + sprintf('Return type of method %s::%s() does not match', $className, $methodName), ); $this->assertSame( $expectedMethodData['isStatic'], $method->isStatic(), - sprintf('Scope of method %s::%s() does not match', $className, $methodName) + sprintf('Scope of method %s::%s() does not match', $className, $methodName), ); $this->assertSame( $expectedMethodData['isVariadic'], $selectedParametersAcceptor->isVariadic(), - sprintf('Method %s::%s() does not match expected variadicity', $className, $methodName) + sprintf('Method %s::%s() does not match expected variadicity', $className, $methodName), ); $this->assertCount( count($expectedMethodData['parameters']), $selectedParametersAcceptor->getParameters(), - sprintf('Method %s::%s() does not match expected count of parameters', $className, $methodName) + sprintf('Method %s::%s() does not match expected count of parameters', $className, $methodName), ); foreach ($selectedParametersAcceptor->getParameters() as $i => $parameter) { $this->assertSame( $expectedMethodData['parameters'][$i]['name'], - $parameter->getName() + $parameter->getName(), ); $this->assertSame( $expectedMethodData['parameters'][$i]['type'], - $parameter->getType()->describe(VerbosityLevel::precise()) + $parameter->getType()->describe(VerbosityLevel::precise()), ); $this->assertTrue( - $expectedMethodData['parameters'][$i]['passedByReference']->equals($parameter->passedByReference()) + $expectedMethodData['parameters'][$i]['passedByReference']->equals($parameter->passedByReference()), ); $this->assertSame( $expectedMethodData['parameters'][$i]['isOptional'], - $parameter->isOptional() + $parameter->isOptional(), ); $this->assertSame( $expectedMethodData['parameters'][$i]['isVariadic'], - $parameter->isVariadic() + $parameter->isVariadic(), ); } } @@ -1020,8 +1025,8 @@ public function testMethods(string $className, array $methods): void public function testOverridingNativeMethodsWithAnnotationsDoesNotBreakGetNativeMethod(): void { - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass(\AnnotationsMethods\Bar::class); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(Bar::class); $this->assertTrue($class->hasNativeMethod('overridenMethodWithAnnotation')); $this->assertInstanceOf(PhpMethodReflection::class, $class->getNativeMethod('overridenMethodWithAnnotation')); } diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php index 0291842501..35d8075f8b 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -2,205 +2,267 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsProperties\Asymmetric; +use AnnotationsProperties\Bar; +use AnnotationsProperties\Baz; +use AnnotationsProperties\BazBaz; +use AnnotationsProperties\Foo; +use AnnotationsProperties\FooInterface; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class AnnotationsPropertiesClassReflectionExtensionTest extends \PHPStan\Testing\TestCase +class AnnotationsPropertiesClassReflectionExtensionTest extends PHPStanTestCase { public function dataProperties(): array { return [ [ - \AnnotationsProperties\Foo::class, + Foo::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Test', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'class' => Foo::class, + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'interfaceProperty' => [ - 'class' => \AnnotationsProperties\FooInterface::class, - 'type' => \AnnotationsProperties\FooInterface::class, + 'class' => FooInterface::class, + 'readableType' => FooInterface::class, + 'writableType' => FooInterface::class, 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], ], ], [ - \AnnotationsProperties\Bar::class, + Bar::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Test', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'class' => Foo::class, + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ - 'class' => \AnnotationsProperties\Bar::class, - 'type' => \AnnotationsProperties\Bar::class, + 'class' => Bar::class, + 'readableType' => Bar::class, + 'writableType' => Bar::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ - 'class' => \AnnotationsProperties\Bar::class, - 'type' => \AnnotationsProperties\Bar::class, + 'class' => Bar::class, + 'readableType' => Bar::class, + 'writableType' => Bar::class, 'writable' => true, 'readable' => true, ], 'conflictingAnnotationProperty' => [ - 'class' => \AnnotationsProperties\Bar::class, - 'type' => \AnnotationsProperties\Bar::class, + 'class' => Bar::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], ], ], [ - \AnnotationsProperties\Baz::class, + Baz::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Test', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'class' => Foo::class, + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\Dolor', + 'class' => Baz::class, + 'readableType' => 'AnnotationsProperties\Dolor', + 'writableType' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\Lorem', + 'class' => Baz::class, + 'readableType' => 'AnnotationsProperties\Lorem', + 'writableType' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\BazBaz', + 'class' => Baz::class, + 'readableType' => 'AnnotationsProperties\BazBaz', + 'writableType' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\Lorem|null', + 'class' => Baz::class, + 'readableType' => '*NEVER*', + 'writableType' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], ], ], [ - \AnnotationsProperties\BazBaz::class, + BazBaz::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Test', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'class' => Foo::class, + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'class' => Foo::class, + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\Dolor', + 'class' => Baz::class, + 'readableType' => 'AnnotationsProperties\Dolor', + 'writableType' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\Lorem', + 'class' => Baz::class, + 'readableType' => 'AnnotationsProperties\Lorem', + 'writableType' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\BazBaz', + 'class' => Baz::class, + 'readableType' => 'AnnotationsProperties\BazBaz', + 'writableType' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, - 'type' => 'AnnotationsProperties\Lorem|null', + 'class' => Baz::class, + 'readableType' => '*NEVER*', + 'writableType' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], 'numericBazBazProperty' => [ - 'class' => \AnnotationsProperties\BazBaz::class, - 'type' => 'float|int', + 'class' => BazBaz::class, + '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, ], @@ -211,52 +273,57 @@ public function dataProperties(): array /** * @dataProvider dataProperties - * @param string $className * @param array $properties */ public function testProperties(string $className, array $properties): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $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), - sprintf('Class %s does not define property %s.', $className, $propertyName) + sprintf('Class %s does not define property %s.', $className, $propertyName), ); $property = $class->getProperty($propertyName, $scope); $this->assertSame( $expectedPropertyData['class'], $property->getDeclaringClass()->getName(), - sprintf('Declaring class of property $%s does not match.', $propertyName) + sprintf('Declaring class of property $%s does not match.', $propertyName), ); $this->assertSame( - $expectedPropertyData['type'], + $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'], $property->isReadable(), - sprintf('Property %s::$%s readability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName) + sprintf('Property %s::$%s readability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName), ); $this->assertSame( $expectedPropertyData['writable'], $property->isWritable(), - sprintf('Property %s::$%s writability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName) + sprintf('Property %s::$%s writability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName), ); } } public function testOverridingNativePropertiesWithAnnotationsDoesNotBreakGetNativeProperty(): void { - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass(\AnnotationsProperties\Bar::class); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(Bar::class); $this->assertTrue($class->hasNativeProperty('overridenPropertyWithAnnotation')); $this->assertSame('AnnotationsProperties\Foo', $class->getNativeProperty('overridenPropertyWithAnnotation')->getReadableType()->describe(VerbosityLevel::precise())); } diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index 56dc5c84fe..48c4197868 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -2,11 +2,23 @@ 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\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use const PHP_VERSION_ID; -class DeprecatedAnnotationsTest extends \PHPStan\Testing\TestCase +class DeprecatedAnnotationsTest extends PHPStanTestCase { public function dataDeprecatedAnnotations(): array @@ -14,7 +26,7 @@ public function dataDeprecatedAnnotations(): array return [ [ false, - \DeprecatedAnnotations\Foo::class, + Foo::class, null, [ 'constant' => [ @@ -32,7 +44,7 @@ public function dataDeprecatedAnnotations(): array ], [ true, - \DeprecatedAnnotations\DeprecatedFoo::class, + DeprecatedFoo::class, 'in 1.0.0.', [ 'constant' => [ @@ -50,7 +62,7 @@ public function dataDeprecatedAnnotations(): array ], [ false, - \DeprecatedAnnotations\FooInterface::class, + FooInterface::class, null, [ 'constant' => [ @@ -64,11 +76,11 @@ public function dataDeprecatedAnnotations(): array ], [ true, - \DeprecatedAnnotations\DeprecatedWithMultipleTags::class, - "in Foo 1.1.0 and will be removed in 1.5.0, use\n\\Foo\\Bar\\NotDeprecated instead.", + DeprecatedWithMultipleTags::class, + "in Foo 1.1.0 and will be removed in 1.5.0, use\n \\Foo\\Bar\\NotDeprecated instead.", [ 'method' => [ - 'deprecatedFoo' => "in Foo 1.1.0, will be removed in Foo 1.5.0, use\n\\Foo\\Bar\\NotDeprecated::replacementFoo() instead.", + 'deprecatedFoo' => "in Foo 1.1.0, will be removed in Foo 1.5.0, use\n \\Foo\\Bar\\NotDeprecated::replacementFoo() instead.", ], ], ], @@ -77,20 +89,18 @@ public function dataDeprecatedAnnotations(): array /** * @dataProvider dataDeprecatedAnnotations - * @param bool $deprecated - * @param string $className - * @param string|null $classDeprecation * @param array $deprecatedAnnotations */ public function testDeprecatedAnnotations(bool $deprecated, string $className, ?string $classDeprecation, array $deprecatedAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $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()); @@ -118,21 +128,277 @@ public function testDeprecatedUserFunctions(): void { require_once __DIR__ . '/data/annotations-deprecated.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name\FullyQualified('DeprecatedAnnotations\foo'), null)->isDeprecated()->yes()); - $this->assertTrue($broker->getFunction(new Name\FullyQualified('DeprecatedAnnotations\deprecatedFoo'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('DeprecatedAnnotations\foo'), null)->isDeprecated()->yes()); + $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('DeprecatedAnnotations\deprecatedFoo'), null)->isDeprecated()->yes()); } public function testNonDeprecatedNativeFunctions(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name('str_replace'), null)->isDeprecated()->yes()); - $this->assertFalse($broker->getFunction(new Name('get_class'), null)->isDeprecated()->yes()); - $this->assertFalse($broker->getFunction(new Name('function_exists'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name('str_replace'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name('get_class'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name('function_exists'), null)->isDeprecated()->yes()); + } + + public function testDeprecatedMethodsFromInterface(): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(DeprecatedBar::class); + $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); + } + + 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 d876b4765f..77b9d3e008 100644 --- a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php @@ -2,11 +2,12 @@ namespace PHPStan\Reflection\Annotations; -use PhpParser\Node\Name; +use FinalAnnotations\FinalFoo; +use FinalAnnotations\Foo; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; -class FinalAnnotationsTest extends \PHPStan\Testing\TestCase +class FinalAnnotationsTest extends PHPStanTestCase { public function dataFinalAnnotations(): array @@ -14,7 +15,7 @@ public function dataFinalAnnotations(): array return [ [ false, - \FinalAnnotations\Foo::class, + Foo::class, [ 'method' => [ 'foo', @@ -24,7 +25,7 @@ public function dataFinalAnnotations(): array ], [ true, - \FinalAnnotations\FinalFoo::class, + FinalFoo::class, [ 'method' => [ 'finalFoo', @@ -37,19 +38,18 @@ public function dataFinalAnnotations(): array /** * @dataProvider dataFinalAnnotations - * @param bool $final - * @param string $className * @param array $finalAnnotations */ public function testFinalAnnotations(bool $final, string $className, array $finalAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); $this->assertSame($final, $class->isFinal()); @@ -59,15 +59,4 @@ public function testFinalAnnotations(bool $final, string $className, array $fina } } - public function testFinalUserFunctions(): void - { - require_once __DIR__ . '/data/annotations-final.php'; - - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - - $this->assertFalse($broker->getFunction(new Name\FullyQualified('FinalAnnotations\foo'), null)->isFinal()->yes()); - $this->assertTrue($broker->getFunction(new Name\FullyQualified('FinalAnnotations\finalFoo'), null)->isFinal()->yes()); - } - } diff --git a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php index 095001f84b..d7af0d248f 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -2,11 +2,17 @@ namespace PHPStan\Reflection\Annotations; +use InternalAnnotations\Foo; +use InternalAnnotations\FooInterface; +use InternalAnnotations\FooTrait; +use InternalAnnotations\InternalFoo; +use InternalAnnotations\InternalFooInterface; +use InternalAnnotations\InternalFooTrait; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; -class InternalAnnotationsTest extends \PHPStan\Testing\TestCase +class InternalAnnotationsTest extends PHPStanTestCase { public function dataInternalAnnotations(): array @@ -14,7 +20,7 @@ public function dataInternalAnnotations(): array return [ [ false, - \InternalAnnotations\Foo::class, + Foo::class, [ 'constant' => [ 'FOO', @@ -31,7 +37,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFoo::class, + InternalFoo::class, [ 'constant' => [ 'INTERNAL_FOO', @@ -48,7 +54,7 @@ public function dataInternalAnnotations(): array ], [ false, - \InternalAnnotations\FooInterface::class, + FooInterface::class, [ 'constant' => [ 'FOO', @@ -61,7 +67,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFooInterface::class, + InternalFooInterface::class, [ 'constant' => [ 'INTERNAL_FOO', @@ -74,7 +80,7 @@ public function dataInternalAnnotations(): array ], [ false, - \InternalAnnotations\FooTrait::class, + FooTrait::class, [ 'method' => [ 'foo', @@ -88,7 +94,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFooTrait::class, + InternalFooTrait::class, [ 'method' => [ 'internalFoo', @@ -105,19 +111,18 @@ public function dataInternalAnnotations(): array /** * @dataProvider dataInternalAnnotations - * @param bool $internal - * @param string $className * @param array $internalAnnotations */ public function testInternalAnnotations(bool $internal, string $className, array $internalAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); $scope->method('canAccessProperty')->willReturn(true); + $scope->method('canReadProperty')->willReturn(true); + $scope->method('canWriteProperty')->willReturn(true); $this->assertSame($internal, $class->isInternal()); @@ -141,11 +146,10 @@ public function testInternalUserFunctions(): void { require_once __DIR__ . '/data/annotations-internal.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name\FullyQualified('InternalAnnotations\foo'), null)->isInternal()->yes()); - $this->assertTrue($broker->getFunction(new Name\FullyQualified('InternalAnnotations\internalFoo'), null)->isInternal()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('InternalAnnotations\foo'), null)->isInternal()->yes()); + $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('InternalAnnotations\internalFoo'), null)->isInternal()->yes()); } } diff --git a/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php index 437fcbe368..7e3f56b299 100644 --- a/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php @@ -4,48 +4,63 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; - -class ThrowsAnnotationsTest extends \PHPStan\Testing\TestCase +use RuntimeException; +use ThrowsAnnotations\BarTrait; +use ThrowsAnnotations\Foo; +use ThrowsAnnotations\FooInterface; +use ThrowsAnnotations\FooTrait; +use ThrowsAnnotations\PhpstanFoo; + +class ThrowsAnnotationsTest extends PHPStanTestCase { public function dataThrowsAnnotations(): array { return [ [ - \ThrowsAnnotations\Foo::class, + Foo::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, + + ], + ], + [ + PhpstanFoo::class, + [ + 'withoutThrows' => 'void', + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\FooInterface::class, + FooInterface::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\FooTrait::class, + FooTrait::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\BarTrait::class, + BarTrait::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], @@ -54,14 +69,12 @@ public function dataThrowsAnnotations(): array /** * @dataProvider dataThrowsAnnotations - * @param string $className * @param array $throwsAnnotations */ public function testThrowsAnnotations(string $className, array $throwsAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); foreach ($throwsAnnotations as $methodName => $type) { @@ -75,24 +88,13 @@ public function testThrowsOnUserFunctions(): void { require_once __DIR__ . '/data/annotations-throws.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertNull($broker->getFunction(new Name\FullyQualified('ThrowsAnnotations\withoutThrows'), null)->getThrowType()); + $this->assertNull($reflectionProvider->getFunction(new Name\FullyQualified('ThrowsAnnotations\withoutThrows'), null)->getThrowType()); - $throwType = $broker->getFunction(new Name\FullyQualified('ThrowsAnnotations\throwsRuntime'), null)->getThrowType(); + $throwType = $reflectionProvider->getFunction(new Name\FullyQualified('ThrowsAnnotations\throwsRuntime'), null)->getThrowType(); $this->assertNotNull($throwType); - $this->assertSame(\RuntimeException::class, $throwType->describe(VerbosityLevel::typeOnly())); - } - - public function testThrowsOnNativeFunctions(): void - { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - - $this->assertNull($broker->getFunction(new Name('str_replace'), null)->getThrowType()); - $this->assertNull($broker->getFunction(new Name('get_class'), null)->getThrowType()); - $this->assertNull($broker->getFunction(new Name('function_exists'), null)->getThrowType()); + $this->assertSame(RuntimeException::class, $throwType->describe(VerbosityLevel::typeOnly())); } } diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php index 0d4db04b06..553c2444ef 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php @@ -187,3 +187,54 @@ public function deprecatedFoo() { } } + +/** + * @deprecated This is totally deprecated. + */ +interface BarInterface +{ + + /** + * @deprecated This is totally deprecated. + */ + public function superDeprecated(); + +} + +/** + * {@inheritdoc} + */ +class DeprecatedBar implements BarInterface +{ + + /** + * {@inheritdoc} + */ + public function superDeprecated() + { + } + +} + +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/annotations-throws.php b/tests/PHPStan/Reflection/Annotations/data/annotations-throws.php index 554c2c2f4a..779e27840c 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-throws.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-throws.php @@ -41,6 +41,40 @@ public static function staticThrowsRuntime() } +class PhpstanFoo +{ + /** + * @throws \RuntimeException + * + * @phpstan-throws void + */ + public function withoutThrows() + { + + } + + /** + * @throws \Exception + * + * @phpstan-throws \RuntimeException + */ + public function throwsRuntime() + { + + } + + /** + * @throws \Exception + * + * @phpstan-throws \RuntimeException + */ + public static function staticThrowsRuntime() + { + + } + +} + interface FooInterface { 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 c6e59041d9..6e4c91a783 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -2,54 +2,64 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\Testing\TestCase; -use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflector\ClassReflector; -use Roave\BetterReflection\Reflector\ConstantReflector; -use Roave\BetterReflection\Reflector\FunctionReflector; +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 TestCase +class AutoloadSourceLocatorTest extends PHPStanTestCase { public function testAutoloadEverythingInFile(): void { - /** @var FunctionReflector $functionReflector */ - $functionReflector = null; - $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class)); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $constantReflector = new ConstantReflector($locator, $classReflector); - $aFoo = $classReflector->reflect(AFoo::class); + $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class), true); + $reflector = new DefaultReflector($locator); + $aFoo = $reflector->reflectClass(AFoo::class); $this->assertNotNull($aFoo->getFileName()); $this->assertSame('a.php', basename($aFoo->getFileName())); - $testFunctionReflection = $functionReflector->reflect('PHPStan\\Reflection\\BetterReflection\\SourceLocator\testFunctionForLocator'); + $testFunctionReflection = $reflector->reflectFunction('PHPStan\\Reflection\\BetterReflection\\SourceLocator\testFunctionForLocator'); $this->assertSame(str_replace('\\', '/', __FILE__), $testFunctionReflection->getFileName()); - $someConstant = $constantReflector->reflect('TestSingleFileSourceLocator\\SOME_CONSTANT'); + $someConstant = $reflector->reflectConstant('TestSingleFileSourceLocator\\SOME_CONSTANT'); $this->assertNotNull($someConstant->getFileName()); $this->assertSame('a.php', basename($someConstant->getFileName())); - $this->assertSame(1, $someConstant->getValue()); - $anotherConstant = $constantReflector->reflect('TestSingleFileSourceLocator\\ANOTHER_CONSTANT'); + $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 = $functionReflector->reflect('TestSingleFileSourceLocator\\doFoo'); + $doFooFunctionReflection = $reflector->reflectFunction('TestSingleFileSourceLocator\\doFoo'); $this->assertSame('TestSingleFileSourceLocator\\doFoo', $doFooFunctionReflection->getName()); $this->assertNotNull($doFooFunctionReflection->getFileName()); $this->assertSame('a.php', basename($doFooFunctionReflection->getFileName())); class_exists(InCondition::class); - $classInCondition = $classReflector->reflect(InCondition::class); + $classInCondition = $reflector->reflectClass(InCondition::class); $classInConditionFilename = $classInCondition->getFileName(); $this->assertNotNull($classInConditionFilename); $this->assertSame('a.php', basename($classInConditionFilename)); @@ -59,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 dd81b2c6fa..8ea1afae71 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php @@ -2,18 +2,24 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\Testing\TestCase; -use Roave\BetterReflection\Reflector\ClassReflector; -use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; -use Roave\BetterReflection\Reflector\FunctionReflector; +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; -class OptimizedDirectorySourceLocatorTest extends TestCase +class OptimizedDirectorySourceLocatorTest extends PHPStanTestCase { - public function dataClass(): array + public function dataClass(): iterable { - return [ + yield from [ [ AFoo::class, AFoo::class, @@ -25,29 +31,54 @@ public function dataClass(): array 'a.php', ], [ - \BFoo::class, - \BFoo::class, + BFoo::class, + BFoo::class, 'b.php', ], [ - 'bfOO', - \BFoo::class, + 'OptimizedDirectory\\bfOO', + BFoo::class, 'b.php', ], + [ + 'TestDirectorySourceLocator\\EmptyClass', + EmptyClass::class, + 'e.php', + ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + 'OptimizedDirectory\\TestEnum', + 'OptimizedDirectory\\TestEnum', + 'enum.php', + ]; + + yield [ + 'OptimizedDirectory\\BackedByStringWithoutSpace', + 'OptimizedDirectory\\BackedByStringWithoutSpace', + 'enum.php', + ]; + + yield [ + 'OptimizedDirectory\\UppercaseEnum', + 'OptimizedDirectory\\UppercaseEnum', + 'enum.php', ]; } /** * @dataProvider dataClass - * @param string $className - * @param string $file */ public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); - $locator = $factory->create(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $classReflection = $classReflector->reflect($className); + $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); + $reflector = new DefaultReflector($locator); + $classReflection = $reflector->reflectClass($className); $this->assertSame($expectedClassName, $classReflection->getName()); $this->assertNotNull($classReflection->getFileName()); $this->assertSame($file, basename($classReflection->getFileName())); @@ -67,18 +98,33 @@ public function dataFunctionExists(): array 'a.php', ], [ - 'doBar', - 'doBar', + 'OptimizedDirectory\\doBar', + 'OptimizedDirectory\\doBar', 'b.php', ], [ - 'doBaz', - 'doBaz', + 'OptimizedDirectory\\doBaz', + 'OptimizedDirectory\\doBaz', 'b.php', ], [ - 'dobaz', - 'doBaz', + 'OptimizedDirectory\\dobaz', + 'OptimizedDirectory\\doBaz', + 'b.php', + ], + [ + 'OptimizedDirectory\\get_smarty', + 'OptimizedDirectory\\get_smarty', + 'b.php', + ], + [ + 'OptimizedDirectory\\get_smarty2', + 'OptimizedDirectory\\get_smarty2', + 'b.php', + ], + [ + 'OptimizedDirectory\\upperCaseFunction', + 'OptimizedDirectory\\upperCaseFunction', 'b.php', ], ]; @@ -86,22 +132,164 @@ public function dataFunctionExists(): array /** * @dataProvider dataFunctionExists - * @param string $functionName - * @param string $expectedFunctionName - * @param string $file */ public function testFunctionExists(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); - $locator = $factory->create(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $functionReflection = $functionReflector->reflect($functionName); + $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); + $reflector = new DefaultReflector($locator); + $functionReflection = $reflector->reflectFunction($functionName); $this->assertSame($expectedFunctionName, $functionReflection->getName()); $this->assertNotNull($functionReflection->getFileName()); $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 [ @@ -112,17 +300,28 @@ public function dataFunctionDoesNotExist(): array /** * @dataProvider dataFunctionDoesNotExist - * @param string $functionName */ public function testFunctionDoesNotExist(string $functionName): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); - $locator = $factory->create(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); + $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); + $reflector = new DefaultReflector($locator); $this->expectException(IdentifierNotFound::class); - $functionReflector->reflect($functionName); + $reflector->reflectFunction($functionName); + } + + public function testBug5525(): void + { + $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); + $locator = $factory->createByFiles([__DIR__ . '/data/bug-5525.php']); + $reflector = new DefaultReflector($locator); + + $class = $reflector->reflectClass('Faker\\Provider\\nl_BE\\Text'); + + /** @var string $className */ + $className = $class->getName(); + $this->assertSame('Faker\\Provider\\nl_BE\\Text', $className); } } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index a9cd7c4d8b..80b588643f 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -2,19 +2,25 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\Testing\TestCase; -use Roave\BetterReflection\Reflector\ClassReflector; -use Roave\BetterReflection\Reflector\ConstantReflector; -use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; -use Roave\BetterReflection\Reflector\FunctionReflector; +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 TestSingleFileSourceLocator\AFoo; +use function array_map; +use const PHP_VERSION_ID; -class OptimizedSingleFileSourceLocatorTest extends TestCase +class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase { - public function dataClass(): array + public function dataClass(): iterable { - return [ + yield from [ [ AFoo::class, AFoo::class, @@ -26,30 +32,110 @@ public function dataClass(): array __DIR__ . '/data/a.php', ], [ - \SingleFileSourceLocatorTestClass::class, - \SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, __DIR__ . '/data/b.php', ], [ 'SinglefilesourceLocatortestClass', - \SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, __DIR__ . '/data/b.php', ], ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + 'OptimizedDirectory\\TestEnum', + 'OptimizedDirectory\\TestEnum', + __DIR__ . '/data/directory/enum.php', + ]; + } + + 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 - * @param string $className - * @param string $expectedClassName - * @param string $file */ public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create($file); - $classReflector = new ClassReflector($locator); - $classReflection = $classReflector->reflect($className); + $reflector = new DefaultReflector($locator); + $classReflection = $reflector->reflectClass($className); $this->assertSame($expectedClassName, $classReflection->getName()); } @@ -81,17 +167,13 @@ public function dataFunction(): array /** * @dataProvider dataFunction - * @param string $functionName - * @param string $expectedFunctionName - * @param string $file */ public function testFunction(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create($file); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $functionReflection = $functionReflector->reflect($functionName); + $reflector = new DefaultReflector($locator); + $functionReflection = $reflector->reflectFunction($functionName); $this->assertSame($expectedFunctionName, $functionReflection->getName()); } @@ -100,33 +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', + 'literal-string&non-falsy-string', + ], + [ + 'OPTIMIZED_SFSL_OBJECT_CONSTANT', + 'stdClass', ], ]; } /** * @dataProvider dataConst - * @param string $constantName - * @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'); - $classReflector = new ClassReflector($locator); - $constantReflector = new ConstantReflector($locator, $classReflector); - $constant = $constantReflector->reflect($constantName); + $reflector = new DefaultReflector($locator); + $constant = $reflector->reflectConstant($constantName); $this->assertSame($constantName, $constant->getName()); - $this->assertSame($value, $constant->getValue()); + + $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 @@ -138,16 +231,38 @@ public function dataConstUnknown(): array /** * @dataProvider dataConstUnknown - * @param string $constantName */ public function testConstUnknown(string $constantName): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create(__DIR__ . '/data/const.php'); - $classReflector = new ClassReflector($locator); - $constantReflector = new ConstantReflector($locator, $classReflector); + $reflector = new DefaultReflector($locator); $this->expectException(IdentifierNotFound::class); - $constantReflector->reflect($constantName); + $reflector->reflectConstant($constantName); + } + + /** + * @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/bug-5525.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/bug-5525.php new file mode 100644 index 0000000000..7bc1aeb4b9 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/bug-5525.php @@ -0,0 +1,25348 @@ += 8.1 + +namespace 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 34ecde8b4a..6fd6ebbd1b 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/directory/b.php @@ -1,8 +1,12 @@ = 8.1 + +namespace OptimizedDirectory; + +enum TestEnum: int +{ + + case ONE = 1; + +} + +enum BackedByStringWithoutSpace:string +{ + // cases +} + +Enum UppercaseEnum:string +{ + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php new file mode 100644 index 0000000000..f38b77dbe9 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php @@ -0,0 +1,6 @@ +createMock(Broker::class); - $fileTypeMapper = $this->createMock(FileTypeMapper::class); - $classReflection = new ClassReflection($broker, $fileTypeMapper, [], [], $className, new \ReflectionClass($className), null, null, null); - $this->assertSame($has, $classReflection->hasTraitUse(\HasTraitUse\FooTrait::class)); + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); + $this->assertSame($has, $classReflection->hasTraitUse(FooTrait::class)); } public function dataClassHierarchyDistances(): array { return [ [ - \HierarchyDistances\Lorem::class, + Lorem::class, [ - \HierarchyDistances\Lorem::class => 0, - \HierarchyDistances\TraitTwo::class => 1, - \HierarchyDistances\TraitThree::class => 2, - \HierarchyDistances\FirstLoremInterface::class => 3, - \HierarchyDistances\SecondLoremInterface::class => 4, + Lorem::class => 0, + TraitTwo::class => 1, + TraitThree::class => 2, + FirstLoremInterface::class => 3, + SecondLoremInterface::class => 4, ], ], [ - \HierarchyDistances\Ipsum::class, - PHP_VERSION_ID < 70400 ? + Ipsum::class, [ - \HierarchyDistances\Ipsum::class => 0, - \HierarchyDistances\TraitOne::class => 1, - \HierarchyDistances\Lorem::class => 2, - \HierarchyDistances\TraitTwo::class => 3, - \HierarchyDistances\TraitThree::class => 4, - \HierarchyDistances\SecondLoremInterface::class => 5, - \HierarchyDistances\FirstLoremInterface::class => 6, - \HierarchyDistances\FirstIpsumInterface::class => 7, - \HierarchyDistances\ExtendedIpsumInterface::class => 8, - \HierarchyDistances\SecondIpsumInterface::class => 9, - \HierarchyDistances\ThirdIpsumInterface::class => 10, - ] - : - [ - \HierarchyDistances\Ipsum::class => 0, - \HierarchyDistances\TraitOne::class => 1, - \HierarchyDistances\Lorem::class => 2, - \HierarchyDistances\TraitTwo::class => 3, - \HierarchyDistances\TraitThree::class => 4, - \HierarchyDistances\FirstLoremInterface::class => 5, - \HierarchyDistances\SecondLoremInterface::class => 6, - \HierarchyDistances\FirstIpsumInterface::class => 7, - \HierarchyDistances\SecondIpsumInterface::class => 8, - \HierarchyDistances\ThirdIpsumInterface::class => 9, - \HierarchyDistances\ExtendedIpsumInterface::class => 10, + Ipsum::class => 0, + TraitOne::class => 1, + Lorem::class => 2, + TraitTwo::class => 3, + TraitThree::class => 4, + FirstLoremInterface::class => 5, + SecondLoremInterface::class => 6, + FirstIpsumInterface::class => 7, + ExtendedIpsumInterface::class => 8, + SecondIpsumInterface::class => 9, + ThirdIpsumInterface::class => 10, ], ], ]; @@ -84,82 +102,564 @@ public function dataClassHierarchyDistances(): array */ public function testClassHierarchyDistances( string $class, - array $expectedDistances + array $expectedDistances, ): void { - $broker = $this->createReflectionProvider(); - $fileTypeMapper = $this->createMock(FileTypeMapper::class); - - $classReflection = new ClassReflection( - $broker, - $fileTypeMapper, - [], - [], - $class, - new \ReflectionClass($class), - null, - null, - null - ); + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass($class); $this->assertSame( $expectedDistances, - $classReflection->getClassHierarchyDistances() + $classReflection->getClassHierarchyDistances(), ); } public function testVariadicTraitMethod(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $fooReflection = $broker->getClass(\HasTraitUse\Foo::class); + $reflectionProvider = $this->createReflectionProvider(); + $fooReflection = $reflectionProvider->getClass(Foo::class); $variadicMethod = $fooReflection->getNativeMethod('variadicMethod'); - $methodVariant = ParametersAcceptorSelector::selectSingle($variadicMethod->getVariants()); + $methodVariant = $variadicMethod->getOnlyVariant(); $this->assertTrue($methodVariant->isVariadic()); } public function testGenericInheritance(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $reflection = $broker->getClass(\GenericInheritance\C::class); + $reflectionProvider = $this->createReflectionProvider(); + $reflection = $reflectionProvider->getClass(C::class); $this->assertSame('GenericInheritance\\C', $reflection->getDisplayName()); $parent = $reflection->getParentClass(); - $this->assertNotFalse($parent); + $this->assertNotNull($parent); $this->assertSame('GenericInheritance\\C0', $parent->getDisplayName()); $this->assertSame([ + 'GenericInheritance\\I', 'GenericInheritance\\I0', 'GenericInheritance\\I1', - 'GenericInheritance\\I', - ], array_map(static function (ClassReflection $r): string { - return $r->getDisplayName(); - }, array_values($reflection->getInterfaces()))); + ], array_map(static fn (ClassReflection $r): string => $r->getDisplayName(), array_values($reflection->getInterfaces()))); } - public function testGenericInheritanceOverride(): void + public function testIsGenericWithStubPhpDoc(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $reflection = $broker->getClass(\GenericInheritance\Override::class); + $reflectionProvider = $this->createReflectionProvider(); + $reflection = $reflectionProvider->getClass(ReflectionClass::class); + $this->assertTrue($reflection->isGeneric()); + } - $this->assertSame([ - 'GenericInheritance\\I0', - 'GenericInheritance\\I1', - 'GenericInheritance\\I', - ], array_map(static function (ClassReflection $r): string { - return $r->getDisplayName(); - }, array_values($reflection->getInterfaces()))); + public function dataIsAttributeClass(): array + { + return [ + [ + IsNotAttribute::class, + false, + ], + [ + IsAttribute::class, + true, + ], + [ + IsAttribute2::class, + true, + Attribute::IS_REPEATABLE, + ], + [ + IsAttribute3::class, + true, + Attribute::IS_REPEATABLE | Attribute::TARGET_PROPERTY, + ], + ]; } - public function testIsGenericWithStubPhpDoc(): void + /** + * @dataProvider dataIsAttributeClass + */ + public function testIsAttributeClass(string $className, bool $expected, int $expectedFlags = Attribute::TARGET_ALL): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $reflection = $broker->getClass(\ReflectionClass::class); - $this->assertTrue($reflection->isGeneric()); + $reflectionProvider = $this->createReflectionProvider(); + $reflection = $reflectionProvider->getClass($className); + $this->assertSame($expected, $reflection->isAttributeClass()); + if (!$expected) { + return; + } + $this->assertSame($expectedFlags, $reflection->getAttributeClassFlags()); + } + + public function testDeprecatedConstantFromAnotherFile(): void + { + $reflectionProvider = $this->createReflectionProvider(); + $reflection = $reflectionProvider->getClass(SecuredRouter::class); + $constant = $reflection->getConstant('SECURED'); + $this->assertTrue($constant->isDeprecated()->yes()); + } + + /** + * @dataProvider dataNestedRecursiveTraits + * @param class-string $className + * @param array $expected + */ + public function testGetTraits(string $className, array $expected, bool $recursive): void + { + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertSame( + array_map( + static fn (ClassReflection $classReflection): string => $classReflection->getNativeReflection()->getName(), + $reflectionProvider->getClass($className)->getTraits($recursive), + ), + $expected, + ); + } + + public function dataNestedRecursiveTraits(): array + { + return [ + [ + NoTrait::class, + [], + false, + ], + [ + NoTrait::class, + [], + true, + ], + [ + \NestedTraits\Foo::class, + [ + \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, + ], + false, + ], + [ + \NestedTraits\Foo::class, + [ + \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, + ], + true, + ], + [ + \NestedTraits\Bar::class, + [ + BarTrait::class => BarTrait::class, + ], + false, + ], + [ + \NestedTraits\Bar::class, + [ + BarTrait::class => BarTrait::class, + \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, + ], + true, + ], + [ + \NestedTraits\Baz::class, + [ + BazTrait::class => BazTrait::class, + ], + false, + ], + [ + \NestedTraits\Baz::class, + [ + BazTrait::class => BazTrait::class, + BarTrait::class => BarTrait::class, + \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, + ], + true, + ], + [ + BazChild::class, + [], + false, + ], + [ + BazChild::class, + [ + BazTrait::class => BazTrait::class, + BarTrait::class => BarTrait::class, + \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, + ], + true, + ], + ]; + } + + public function testEnumIsFinal(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $reflectionProvider = $this->createReflectionProvider(); + $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); + $this->assertTrue($enum->isEnum()); + $this->assertInstanceOf('ReflectionEnum', $enum->getNativeReflection()); // @phpstan-ignore-line Exact error differs on PHP 7.4 and others + $this->assertTrue($enum->isFinal()); + $this->assertTrue($enum->isFinalByKeyword()); + } + + public function testBackedEnumType(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $reflectionProvider = $this->createReflectionProvider(); + $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); + $this->assertInstanceOf(IntegerType::class, $enum->getBackedEnumType()); + } + + 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 909057cfb5..94306bc0ec 100644 --- a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php +++ b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateTypeFactory; @@ -13,12 +14,14 @@ 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; +use function count; +use function get_class; +use function sprintf; -class GenericParametersAcceptorResolverTest extends \PHPStan\Testing\TestCase +class GenericParametersAcceptorResolverTest extends PHPStanTestCase { /** @@ -26,14 +29,12 @@ class GenericParametersAcceptorResolverTest extends \PHPStan\Testing\TestCase */ public function dataResolve(): array { - $templateType = static function (string $name, ?Type $type = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $type, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name, ?Type $type = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $type, + TemplateTypeVariance::createInvariant(), + ); return [ 'one param, one arg' => [ @@ -52,11 +53,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -70,11 +71,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'two params, two args, return type' => [ @@ -95,7 +96,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -103,11 +104,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - $templateType('U') + $templateType('U'), ), new FunctionVariant( new TemplateTypeMap([ @@ -122,7 +123,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -130,11 +131,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new IntegerType() + new IntegerType(), ), ], 'mixed types' => [ @@ -154,7 +155,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -162,11 +163,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - $templateType('T') + $templateType('T'), ), new FunctionVariant( new TemplateTypeMap([ @@ -183,7 +184,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -194,14 +195,14 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, new UnionType([ new ObjectType('DateTime'), new IntegerType(), - ]) + ]), ), ], 'parameter default value' => [ @@ -221,7 +222,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -229,11 +230,11 @@ public function dataResolve(): array true, PassedByReference::createNo(), false, - new IntegerType() + new IntegerType(), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -248,7 +249,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -256,11 +257,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'variadic parameter' => [ @@ -283,7 +284,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -291,11 +292,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), true, - null + null, ), ], true, - $templateType('U') + $templateType('U'), ), new FunctionVariant( new TemplateTypeMap([ @@ -310,19 +311,27 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', - new IntegerType(), + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ]), false, PassedByReference::createNo(), true, - null + null, ), ], false, - new IntegerType() + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ]), ), ], 'missing args' => [ @@ -342,7 +351,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -350,11 +359,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -369,7 +378,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -377,11 +386,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'constant string arg resolved to constant string' => [ @@ -397,16 +406,16 @@ public function dataResolve(): array new DummyParameter('str', $templateType('T'), false, null, false, null), ], false, - $templateType('T') + $templateType('T'), ), new FunctionVariant( 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'), ), ], ]; @@ -414,24 +423,25 @@ public function dataResolve(): array /** * @dataProvider dataResolve - * @param \PHPStan\Type\Type[] $argTypes + * @param Type[] $argTypes */ public function testResolve(array $argTypes, ParametersAcceptor $parametersAcceptor, ParametersAcceptor $expectedResult): void { + self::getContainer(); // to initialize bleeding edge $result = GenericParametersAcceptorResolver::resolve( $argTypes, - $parametersAcceptor + $parametersAcceptor, ); $this->assertInstanceOf( get_class($expectedResult->getReturnType()), $result->getReturnType(), - 'Unexpected return type' + 'Unexpected return type', ); $this->assertSame( $expectedResult->getReturnType()->describe(VerbosityLevel::precise()), $result->getReturnType()->describe(VerbosityLevel::precise()), - 'Unexpected return type' + 'Unexpected return type', ); $resultParameters = $result->getParameters(); @@ -443,14 +453,21 @@ public function testResolve(array $argTypes, ParametersAcceptor $parametersAccep $this->assertInstanceOf( get_class($param->getType()), $resultParameters[$i]->getType(), - sprintf('Unexpected parameter %d', $i + 1) + sprintf('Unexpected parameter %d', $i + 1), ); $this->assertSame( $param->getType()->describe(VerbosityLevel::precise()), $resultParameters[$i]->getType()->describe(VerbosityLevel::precise()), - sprintf('Unexpected parameter %d', $i + 1) + sprintf('Unexpected parameter %d', $i + 1), ); } } + 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 new file mode 100644 index 0000000000..6ccc847d84 --- /dev/null +++ b/tests/PHPStan/Reflection/MixedTypeTest.php @@ -0,0 +1,47 @@ +createReflectionProvider(); + $class = $reflectionProvider->getClass(Foo::class); + $propertyType = $class->getNativeProperty('fooProp')->getNativeType(); + $this->assertInstanceOf(MixedType::class, $propertyType); + $this->assertTrue($propertyType->isExplicitMixed()); + + $method = $class->getNativeMethod('doFoo'); + $methodVariant = $method->getOnlyVariant(); + $methodReturnType = $methodVariant->getReturnType(); + $this->assertInstanceOf(MixedType::class, $methodReturnType); + $this->assertTrue($methodReturnType->isExplicitMixed()); + + $methodParameterType = $methodVariant->getParameters()[0]->getType(); + $this->assertInstanceOf(MixedType::class, $methodParameterType); + $this->assertTrue($methodParameterType->isExplicitMixed()); + + $function = $reflectionProvider->getFunction(new Name('NativeMixedType\doFoo'), null); + $functionVariant = $function->getOnlyVariant(); + $functionReturnType = $functionVariant->getReturnType(); + $this->assertInstanceOf(MixedType::class, $functionReturnType); + $this->assertTrue($functionReturnType->isExplicitMixed()); + + $functionParameterType = $functionVariant->getParameters()[0]->getType(); + $this->assertInstanceOf(MixedType::class, $functionParameterType); + $this->assertTrue($functionParameterType->isExplicitMixed()); + } + +} diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index ecf59410e3..1f2e534a45 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -2,9 +2,14 @@ namespace PHPStan\Reflection; +use DateInterval; +use DateTimeInterface; +use Generator; use PhpParser\Node\Name; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -14,23 +19,26 @@ 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; use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; -class ParametersAcceptorSelectorTest extends \PHPStan\Testing\TestCase +class ParametersAcceptorSelectorTest extends PHPStanTestCase { - public function dataSelectFromTypes(): \Generator + public function dataSelectFromTypes(): Generator { require_once __DIR__ . '/data/function-definitions.php'; - $broker = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); - $arrayRandVariants = $broker->getFunction(new Name('array_rand'), null)->getVariants(); + $arrayRandVariants = $reflectionProvider->getFunction(new Name('array_rand'), null)->getVariants(); yield [ [ new ArrayType(new MixedType(), new MixedType()), @@ -50,11 +58,11 @@ public function dataSelectFromTypes(): \Generator $arrayRandVariants[1], ]; - $datePeriodConstructorVariants = $broker->getClass('DatePeriod')->getNativeMethod('__construct')->getVariants(); + $datePeriodConstructorVariants = $reflectionProvider->getClass('DatePeriod')->getNativeMethod('__construct')->getVariants(); yield [ [ - new ObjectType(\DateTimeInterface::class), - new ObjectType(\DateInterval::class), + new ObjectType(DateTimeInterface::class), + new ObjectType(DateInterval::class), new IntegerType(), new IntegerType(), ], @@ -64,9 +72,9 @@ public function dataSelectFromTypes(): \Generator ]; yield [ [ - new ObjectType(\DateTimeInterface::class), - new ObjectType(\DateInterval::class), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), + new ObjectType(DateInterval::class), + new ObjectType(DateTimeInterface::class), new IntegerType(), ], $datePeriodConstructorVariants, @@ -83,7 +91,7 @@ public function dataSelectFromTypes(): \Generator $datePeriodConstructorVariants[2], ]; - $ibaseWaitEventVariants = $broker->getFunction(new Name('ibase_wait_event'), null)->getVariants(); + $ibaseWaitEventVariants = $reflectionProvider->getFunction(new Name('ibase_wait_event'), null)->getVariants(); yield [ [ new ResourceType(), @@ -120,7 +128,7 @@ public function dataSelectFromTypes(): \Generator new MixedType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'event|args', @@ -128,44 +136,15 @@ public function dataSelectFromTypes(): \Generator new MixedType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new StringType() - ), - ]; - - $absVariants = $broker->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 = $broker->getFunction(new Name('strtok'), null)->getVariants(); + $strtokVariants = $reflectionProvider->getFunction(new Name('strtok'), null)->getVariants(); yield [ [], $strtokVariants, @@ -180,7 +159,7 @@ public function dataSelectFromTypes(): \Generator new StringType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'token', @@ -188,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 [ @@ -215,7 +194,7 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'intVariadic', @@ -223,11 +202,11 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new IntegerType() + new IntegerType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -239,7 +218,7 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'floatVariadic', @@ -247,11 +226,11 @@ public function dataSelectFromTypes(): \Generator new FloatType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new IntegerType() + new IntegerType(), ), ]; @@ -284,11 +263,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(1) + new ConstantIntegerType(1), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -300,11 +279,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(2) + new ConstantIntegerType(2), ), ], false, - new NullType() + new NullType(), ), ]; @@ -327,11 +306,11 @@ public function dataSelectFromTypes(): \Generator new UnionType([ new ConstantIntegerType(1), new ConstantIntegerType(2), - ]) + ]), ), ], false, - new NullType() + new NullType(), ), ]; @@ -346,11 +325,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(1) + new ConstantIntegerType(1), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -362,11 +341,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -386,11 +365,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -405,16 +384,16 @@ public function dataSelectFromTypes(): \Generator TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -434,27 +413,25 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; } /** * @dataProvider dataSelectFromTypes - * @param \PHPStan\Type\Type[] $types + * @param Type[] $types * @param ParametersAcceptor[] $variants - * @param bool $unpack - * @param ParametersAcceptor $expected */ public function testSelectFromTypes( array $types, array $variants, bool $unpack, - ParametersAcceptor $expected + ParametersAcceptor $expected, ): void { $selectedAcceptor = ParametersAcceptorSelector::selectFromTypes($types, $variants, $unpack); @@ -463,36 +440,36 @@ public function testSelectFromTypes( $expectedParameter = $expected->getParameters()[$i]; $this->assertSame( $expectedParameter->getName(), - $parameter->getName() + $parameter->getName(), ); $this->assertSame( $expectedParameter->isOptional(), - $parameter->isOptional() + $parameter->isOptional(), ); $this->assertSame( $expectedParameter->getType()->describe(VerbosityLevel::precise()), - $parameter->getType()->describe(VerbosityLevel::precise()) + $parameter->getType()->describe(VerbosityLevel::precise()), ); $this->assertTrue( - $expectedParameter->passedByReference()->equals($parameter->passedByReference()) + $expectedParameter->passedByReference()->equals($parameter->passedByReference()), ); $this->assertSame( $expectedParameter->isVariadic(), - $parameter->isVariadic() + $parameter->isVariadic(), ); if ($expectedParameter->getDefaultValue() === null) { $this->assertNull($parameter->getDefaultValue()); } else { $this->assertSame( $expectedParameter->getDefaultValue()->describe(VerbosityLevel::precise()), - $parameter->getDefaultValue() !== null ? $parameter->getDefaultValue()->describe(VerbosityLevel::precise()) : null + $parameter->getDefaultValue() !== null ? $parameter->getDefaultValue()->describe(VerbosityLevel::precise()) : null, ); } } $this->assertSame( $expected->getReturnType()->describe(VerbosityLevel::precise()), - $selectedAcceptor->getReturnType()->describe(VerbosityLevel::precise()) + $selectedAcceptor->getReturnType()->describe(VerbosityLevel::precise()), ); $this->assertSame($expected->isVariadic(), $selectedAcceptor->isVariadic()); } diff --git a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php index 0c5c5d812b..dabd3948e0 100644 --- a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php @@ -2,45 +2,74 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; +use stdClass; -class UniversalObjectCratesClassReflectionExtensionTest extends \PHPStan\Testing\TestCase +class UniversalObjectCratesClassReflectionExtensionTest extends PHPStanTestCase { public function testNonexistentClass(): void { - $broker = self::getContainer()->getByType(Broker::class); - $extension = new UniversalObjectCratesClassReflectionExtension([ - 'NonexistentClass', - 'stdClass', - ]); - $extension->setBroker($broker); - $this->assertTrue($extension->hasProperty($broker->getClass(\stdClass::class), 'foo')); + $reflectionProvider = $this->createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + ['NonexistentClass', 'stdClass'], + new AnnotationsPropertiesClassReflectionExtension(), + ); + $this->assertTrue($extension->hasProperty($reflectionProvider->getClass(stdClass::class), 'foo')); } public function testDifferentGetSetType(): void { require_once __DIR__ . '/data/universal-object-crates.php'; - $broker = self::getContainer()->getByType(Broker::class); - $extension = new UniversalObjectCratesClassReflectionExtension([ - 'UniversalObjectCreates\DifferentGetSetTypes', - ]); - $extension->setBroker($broker); + $reflectionProvider = $this->createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension( + $reflectionProvider, + ['UniversalObjectCreates\DifferentGetSetTypes'], + new AnnotationsPropertiesClassReflectionExtension(), + ); $this->assertEquals( new ObjectType('UniversalObjectCreates\DifferentGetSetTypesValue'), $extension - ->getProperty($broker->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') - ->getReadableType() + ->getProperty($reflectionProvider->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') + ->getReadableType(), + ); + $this->assertEquals( + new StringType(), + $extension + ->getProperty($reflectionProvider->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') + ->getWritableType(), + ); + } + + 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($broker->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') - ->getWritableType() + ->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 new file mode 100644 index 0000000000..db8896c9cb --- /dev/null +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -0,0 +1,161 @@ += 80000) { + yield [ + 'bcdiv', + new ObjectType('DivisionByZeroError'), + ]; + } else { + yield [ + 'bcdiv', + null, + ]; + } + + yield [ + 'GEOSRelateMatch', + new ObjectType('Exception'), + ]; + + yield [ + 'random_int', + new ObjectType('Random\RandomException'), + ]; + } + + /** + * @dataProvider dataFunctionThrowType + * + * @param non-empty-string $functionName + */ + public function testFunctionThrowType(string $functionName, ?Type $expectedThrowType): void + { + $reflectionProvider = $this->createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name($functionName), null); + $throwType = $function->getThrowType(); + if ($expectedThrowType === null) { + $this->assertNull($throwType); + return; + } + $this->assertNotNull($throwType); + $this->assertSame( + $expectedThrowType->describe(VerbosityLevel::precise()), + $throwType->describe(VerbosityLevel::precise()), + ); + } + + public function dataFunctionDeprecated(): iterable + { + if (PHP_VERSION_ID < 80000) { + yield 'create_function' => [ + 'create_function', + true, + ]; + yield 'each' => [ + 'each', + true, + ]; + } + + if (PHP_VERSION_ID < 90000) { + yield 'date_sunrise' => [ + 'date_sunrise', + PHP_VERSION_ID >= 80100, + ]; + } + + yield 'strtolower' => [ + 'strtolower', + false, + ]; + } + + /** + * @dataProvider dataFunctionDeprecated + * + * @param non-empty-string $functionName + */ + public function testFunctionDeprecated(string $functionName, bool $isDeprecated): void + { + $reflectionProvider = $this->createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name($functionName), null); + $this->assertEquals(TrinaryLogic::createFromBoolean($isDeprecated), $function->isDeprecated()); + } + + public function dataMethodThrowType(): array + { + return [ + [ + DateTime::class, + '__construct', + PHP_VERSION_ID >= 80300 ? new ObjectType('DateMalformedStringException') : new ObjectType('Exception'), + ], + [ + DateTime::class, + 'format', + null, + ], + ]; + } + + /** + * @dataProvider dataMethodThrowType + */ + public function testMethodThrowType(string $className, string $methodName, ?Type $expectedThrowType): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $method = $class->getNativeMethod($methodName); + $throwType = $method->getThrowType(); + if ($expectedThrowType === null) { + $this->assertNull($throwType); + return; + } + $this->assertNotNull($throwType); + $this->assertSame( + $expectedThrowType->describe(VerbosityLevel::precise()), + $throwType->describe(VerbosityLevel::precise()), + ); + } + + 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/FunctionMetadataTest.php b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php index 731397674b..24ef8431ee 100644 --- a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php @@ -4,21 +4,21 @@ use Nette\Schema\Expect; use Nette\Schema\Processor; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -class FunctionMetadataTest extends TestCase +class FunctionMetadataTest extends PHPStanTestCase { public function testSchema(): void { - $data = require __DIR__ . '/../../../../src/Reflection/SignatureMap/functionMetadata.php'; + $data = require __DIR__ . '/../../../../resources/functionMetadata.php'; $this->assertIsArray($data); $processor = new Processor(); $processor->process(Expect::arrayOf( Expect::structure([ 'hasSideEffects' => Expect::bool()->required(), - ])->required() + ])->required(), )->required(), $data); } diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php new file mode 100644 index 0000000000..182a6582e9 --- /dev/null +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -0,0 +1,326 @@ + 'url', + 'optional' => true, + 'type' => new UnionType([ + new StringType(), + new NullType(), + ]), + 'nativeType' => new UnionType([ + new StringType(), + new NullType(), + ]), + 'passedByReference' => PassedByReference::createNo(), + 'variadic' => false, + ], + ], + new BenevolentUnionType([ + new ObjectType('CurlHandle'), + new ConstantBooleanType(false), + ]), + new UnionType([ + new ObjectType('CurlHandle'), + new ConstantBooleanType(false), + ]), + false, + ], + [ + 'curl_exec', + [ + [ + 'name' => 'handle', + 'optional' => false, + 'type' => new ObjectType('CurlHandle'), + 'nativeType' => new ObjectType('CurlHandle'), + 'passedByReference' => PassedByReference::createNo(), + 'variadic' => false, + ], + ], + new UnionType([new StringType(), new BooleanType()]), + new UnionType([new StringType(), new BooleanType()]), + false, + ], + [ + 'date_get_last_errors', + [], + new UnionType([ + new ConstantBooleanType(false), + new ConstantArrayType([ + new ConstantStringType('warning_count'), + new ConstantStringType('warnings'), + new ConstantStringType('error_count'), + new ConstantStringType('errors'), + ], [ + 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(), new MixedType()), + ]), + false, + ], + [ + 'end', + [ + [ + 'name' => 'array', + 'optional' => false, + 'type' => new UnionType([new ArrayType(new MixedType(), new MixedType()), new ObjectWithoutClassType()]), + 'nativeType' => new UnionType([new ArrayType(new MixedType(), new MixedType()), new ObjectWithoutClassType()]), + 'passedByReference' => PassedByReference::createReadsArgument(), + 'variadic' => false, + ], + ], + new MixedType(true), + new MixedType(true), + false, + ], + ]; + } + + /** + * @dataProvider dataFunctions + * @param mixed[] $parameters + */ + public function testFunctions( + string $functionName, + array $parameters, + Type $returnType, + Type $nativeReturnType, + bool $variadic, + ): void + { + $provider = $this->createProvider(); + $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), + 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), + ); + } + + public function dataMethods(): array + { + return [ + [ + 'Closure', + 'bindTo', + [ + [ + 'name' => 'newThis', + 'optional' => false, + 'type' => new UnionType([ + new ObjectWithoutClassType(), + new NullType(), + ]), + 'nativeType' => new UnionType([ + new ObjectWithoutClassType(), + new NullType(), + ]), + 'passedByReference' => PassedByReference::createNo(), + 'variadic' => false, + ], + [ + 'name' => 'newScope', + 'optional' => true, + 'type' => new UnionType([ + new ObjectWithoutClassType(), + new ClassStringType(), + new ConstantStringType('static'), + new NullType(), + ]), + 'nativeType' => new UnionType([ + new ObjectWithoutClassType(), + new StringType(), + new NullType(), + ]), + 'passedByReference' => PassedByReference::createNo(), + 'variadic' => false, + ], + ], + new BenevolentUnionType([ + new ObjectType('Closure'), + new NullType(), + ]), + new UnionType([ + new ObjectType('Closure'), + new NullType(), + ]), + false, + ], + [ + 'ArrayIterator', + 'uasort', + [ + [ + 'name' => 'callback', + 'optional' => false, + 'type' => new CallableType([ + new NativeParameterReflection('', false, new MixedType(true), PassedByReference::createNo(), false, null), + new NativeParameterReflection('', false, new MixedType(true), PassedByReference::createNo(), false, null), + ], new IntegerType(), false), + 'nativeType' => new CallableType(), + 'passedByReference' => PassedByReference::createNo(), + 'variadic' => false, + ], + ], + new VoidType(), + new MixedType(), + false, + ], + [ + 'RecursiveArrayIterator', + 'uasort', + [ + [ + 'name' => 'callback', + 'optional' => false, + 'type' => new CallableType([ + new NativeParameterReflection('', false, new MixedType(true), PassedByReference::createNo(), false, null), + new NativeParameterReflection('', false, new MixedType(true), PassedByReference::createNo(), false, null), + ], new IntegerType(), false), + 'nativeType' => new MixedType(), // todo - because uasort is not found in file with RecursiveArrayIterator + 'passedByReference' => PassedByReference::createNo(), + 'variadic' => false, + ], + ], + new VoidType(), + new MixedType(), // todo - because uasort is not found in file with RecursiveArrayIterator + false, + ], + ]; + } + + /** + * @dataProvider dataMethods + * @param mixed[] $parameters + */ + public function testMethods( + string $className, + string $methodName, + array $parameters, + Type $returnType, + Type $nativeReturnType, + bool $variadic, + ): void + { + $provider = $this->createProvider(); + $signatures = $provider->getMethodSignatures($className, $methodName, null)['positional']; + $this->assertCount(1, $signatures); + $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); + } + + /** + * @param mixed[] $expectedParameters + */ + private function assertSignature( + array $expectedParameters, + Type $expectedReturnType, + Type $expectedNativeReturnType, + bool $expectedVariadic, + FunctionSignature $actualSignature, + ): void + { + $this->assertCount(count($expectedParameters), $actualSignature->getParameters()); + foreach ($expectedParameters as $i => $expectedParameter) { + $actualParameter = $actualSignature->getParameters()[$i]; + $this->assertSame($expectedParameter['name'], $actualParameter->getName()); + $this->assertSame($expectedParameter['optional'], $actualParameter->isOptional()); + $this->assertSame($expectedParameter['type']->describe(VerbosityLevel::precise()), $actualParameter->getType()->describe(VerbosityLevel::precise())); + $this->assertSame($expectedParameter['nativeType']->describe(VerbosityLevel::precise()), $actualParameter->getNativeType()->describe(VerbosityLevel::precise())); + $this->assertTrue($expectedParameter['passedByReference']->equals($actualParameter->passedByReference())); + $this->assertSame($expectedParameter['variadic'], $actualParameter->isVariadic()); + } + + $this->assertSame($expectedReturnType->describe(VerbosityLevel::precise()), $actualSignature->getReturnType()->describe(VerbosityLevel::precise())); + $this->assertSame($expectedNativeReturnType->describe(VerbosityLevel::precise()), $actualSignature->getNativeReturnType()->describe(VerbosityLevel::precise())); + $this->assertSame($expectedVariadic, $actualSignature->isVariadic()); + } + + public function dataParseAll(): array + { + $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)); + } + + /** + * @dataProvider dataParseAll + */ + public function testParseAll(string $stubFile): void + { + $parser = $this->getParser(); + $parser->parseFile($stubFile); + $this->expectNotToPerformAssertions(); + } + +} diff --git a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php index 93b4dce644..00ed1a4159 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -2,7 +2,19 @@ namespace PHPStan\Reflection\SignatureMap; +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; use PHPStan\Type\CallableType; @@ -16,12 +28,20 @@ use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use ReflectionParameter; +use Throwable; +use function array_keys; +use function count; +use function explode; +use function sprintf; +use function strpos; -class SignatureMapParserTest extends \PHPStan\Testing\TestCase +class SignatureMapParserTest extends PHPStanTestCase { public function dataGetFunctions(): array { + $reflectionProvider = $this->createReflectionProvider(); return [ [ ['int', 'fp' => 'resource', 'fields' => 'array', 'delimiter=' => 'string', 'enclosure=' => 'string', 'escape_char=' => 'string'], @@ -32,40 +52,56 @@ public function dataGetFunctions(): array 'fp', false, new ResourceType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'fields', false, new ArrayType(new MixedType(), new MixedType()), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'delimiter', true, new StringType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'enclosure', true, new StringType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'escape_char', true, new StringType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], new IntegerType(), - false + new MixedType(), + false, ), ], [ @@ -77,12 +113,16 @@ public function dataGetFunctions(): array 'fp', false, new ResourceType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], new BooleanType(), - false + new MixedType(), + false, ), ], [ @@ -94,12 +134,16 @@ public function dataGetFunctions(): array 'array_arg', false, new ArrayType(new MixedType(), new MixedType()), + new MixedType(), PassedByReference::createReadsArgument(), - false + false, + null, + null, ), ], new BooleanType(), - false + new MixedType(), + false, ), ], [ @@ -114,26 +158,36 @@ public function dataGetFunctions(): array new StringType(), new ResourceType(), ]), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'out', false, new StringType(), + new MixedType(), PassedByReference::createCreatesNewVariable(), - false + false, + null, + null, ), new ParameterSignature( 'notext', true, new BooleanType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], new BooleanType(), - false + new MixedType(), + false, ), ], [ @@ -142,11 +196,12 @@ public function dataGetFunctions(): array new FunctionSignature( [], new UnionType([ - new ObjectType(\Throwable::class), + new ObjectType(Throwable::class), new ObjectType('Foo'), new NullType(), ]), - false + new MixedType(), + false, ), ], [ @@ -155,7 +210,8 @@ public function dataGetFunctions(): array new FunctionSignature( [], new MixedType(), - false + new MixedType(), + false, ), ], [ @@ -167,26 +223,36 @@ public function dataGetFunctions(): array 'arr1', false, new ArrayType(new MixedType(), new MixedType()), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'arr2', false, new ArrayType(new MixedType(), new MixedType()), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( '...', true, new ArrayType(new MixedType(), new MixedType()), + new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new ArrayType(new MixedType(), new MixedType()), - true + new MixedType(), + true, ), ], [ @@ -198,26 +264,36 @@ public function dataGetFunctions(): array 'callback', false, new CallableType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'event', false, new StringType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( '...', true, new MixedType(), + new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new ResourceType(), - true + new MixedType(), + true, ), ], [ @@ -229,19 +305,26 @@ public function dataGetFunctions(): array 'format', false, new StringType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'args', true, new MixedType(), + new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new StringType(), - true + new MixedType(), + true, ), ], [ @@ -253,19 +336,26 @@ public function dataGetFunctions(): array 'format', false, new StringType(), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), new ParameterSignature( 'args', true, new MixedType(), + new MixedType(), PassedByReference::createNo(), - true + true, + null, + null, ), ], new StringType(), - true + new MixedType(), + true, ), ], [ @@ -273,25 +363,30 @@ public function dataGetFunctions(): array null, new FunctionSignature( [], - new ArrayType(new IntegerType(), new ObjectType(\ReflectionParameter::class)), - false + new ArrayType(new IntegerType(), new ObjectType(ReflectionParameter::class)), + new MixedType(), + false, ), ], [ ['static', 'interval' => 'DateInterval'], - \DateTime::class, + DateTime::class, new FunctionSignature( [ new ParameterSignature( 'interval', false, - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), + new MixedType(), PassedByReference::createNo(), - false + false, + null, + null, ), ], - new StaticType(\DateTime::class), - false + new StaticType($reflectionProvider->getClass(DateTime::class)), + new MixedType(), + false, ), ], [ @@ -303,19 +398,26 @@ public function dataGetFunctions(): array 'string', false, new StringType(), + new MixedType(), PassedByReference::createReadsArgument(), - false + false, + null, + null, ), new ParameterSignature( 'strings', true, new StringType(), + new MixedType(), PassedByReference::createReadsArgument(), - true + true, + null, + null, ), ], new BooleanType(), - true + new MixedType(), + true, ), ], ]; @@ -324,13 +426,11 @@ public function dataGetFunctions(): array /** * @dataProvider dataGetFunctions * @param mixed[] $map - * @param string|null $className - * @param \PHPStan\Reflection\SignatureMap\FunctionSignature $expectedSignature */ public function testGetFunctions( array $map, ?string $className, - FunctionSignature $expectedSignature + FunctionSignature $expectedSignature, ): void { /** @var SignatureMapParser $parser */ @@ -339,7 +439,7 @@ public function testGetFunctions( $this->assertCount( count($expectedSignature->getParameters()), $functionSignature->getParameters(), - 'Number of parameters does not match.' + 'Number of parameters does not match.', ); foreach ($functionSignature->getParameters() as $i => $parameterSignature) { @@ -347,64 +447,111 @@ public function testGetFunctions( $this->assertSame( $expectedParameterSignature->getName(), $parameterSignature->getName(), - sprintf('Name of parameter #%d does not match.', $i) + sprintf('Name of parameter #%d does not match.', $i), ); $this->assertSame( $expectedParameterSignature->isOptional(), $parameterSignature->isOptional(), - sprintf('Optionality of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Optionality of parameter $%s does not match.', $parameterSignature->getName()), ); $this->assertSame( $expectedParameterSignature->getType()->describe(VerbosityLevel::precise()), $parameterSignature->getType()->describe(VerbosityLevel::precise()), - sprintf('Type of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Type of parameter $%s does not match.', $parameterSignature->getName()), ); $this->assertTrue( $expectedParameterSignature->passedByReference()->equals($parameterSignature->passedByReference()), - sprintf('Passed-by-reference of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Passed-by-reference of parameter $%s does not match.', $parameterSignature->getName()), ); $this->assertSame( $expectedParameterSignature->isVariadic(), $parameterSignature->isVariadic(), - sprintf('Variadicity of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Variadicity of parameter $%s does not match.', $parameterSignature->getName()), ); } $this->assertSame( $expectedSignature->getReturnType()->describe(VerbosityLevel::precise()), $functionSignature->getReturnType()->describe(VerbosityLevel::precise()), - 'Return type does not match.' + 'Return type does not match.', ); $this->assertSame( $expectedSignature->isVariadic(), $functionSignature->isVariadic(), - 'Variadicity does not match.' + 'Variadicity does not match.', ); } - public function testParseAll(): void + public function dataParseAll(): array + { + return [ + [70400], + [80000], + ]; + } + + /** + * @dataProvider dataParseAll + */ + public function testParseAll(int $phpVersionId): void { $parser = self::getContainer()->getByType(SignatureMapParser::class); - $signatureMap = require __DIR__ . '/../../../../src/Reflection/SignatureMap/functionMap.php'; + $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 ($signatureMap as $functionName => $map) { + foreach (array_keys($signatureMap) as $functionName) { $className = null; 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 = $parser->getFunctionSignature($map, $className); - $count++; - } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) { - $this->fail(sprintf('Could not parse %s.', $functionName)); + $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); - foreach ($signature->getParameters() as $parameter) { - self::assertNotInstanceOf(ErrorType::class, $parameter->getType(), sprintf('%s (parameter %s)', $functionName, $parameter->getName())); + 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())); + } } } diff --git a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php new file mode 100644 index 0000000000..2d7d3ca357 --- /dev/null +++ b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php @@ -0,0 +1,47 @@ +createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated'), + $this->createDeprecatedMethod(TrinaryLogic::createMaybe(), 'Maybe deprecated'), + $this->createDeprecatedMethod(TrinaryLogic::createNo(), 'Not deprecated'), + ], + ); + + $this->assertSame('Deprecated', $reflection->getDeprecatedDescription()); + } + + public function testMultipleDeprecationsAreJoined(): void + { + $reflection = new IntersectionTypeMethodReflection( + 'foo', + [ + $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #1'), + $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #2'), + ], + ); + + $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); + } + + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection + { + $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 new file mode 100644 index 0000000000..b41d8d9636 --- /dev/null +++ b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php @@ -0,0 +1,47 @@ +createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated'), + $this->createDeprecatedMethod(TrinaryLogic::createMaybe(), 'Maybe deprecated'), + $this->createDeprecatedMethod(TrinaryLogic::createNo(), 'Not deprecated'), + ], + ); + + $this->assertSame('Deprecated', $reflection->getDeprecatedDescription()); + } + + public function testMultipleDeprecationsAreJoined(): void + { + $reflection = new UnionTypeMethodReflection( + 'foo', + [ + $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #1'), + $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #2'), + ], + ); + + $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); + } + + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection + { + $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 new file mode 100644 index 0000000000..79fb96b28a --- /dev/null +++ b/tests/PHPStan/Reflection/UnionTypesTest.php @@ -0,0 +1,45 @@ +createReflectionProvider(); + $class = $reflectionProvider->getClass(Foo::class); + $propertyType = $class->getNativeProperty('fooProp')->getNativeType(); + $this->assertInstanceOf(UnionType::class, $propertyType); + $this->assertSame('bool|int', $propertyType->describe(VerbosityLevel::precise())); + + $method = $class->getNativeMethod('doFoo'); + $methodVariant = $method->getOnlyVariant(); + $methodReturnType = $methodVariant->getReturnType(); + $this->assertInstanceOf(UnionType::class, $methodReturnType); + $this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $methodReturnType->describe(VerbosityLevel::precise())); + + $methodParameterType = $methodVariant->getParameters()[0]->getType(); + $this->assertInstanceOf(UnionType::class, $methodParameterType); + $this->assertSame('bool|int', $methodParameterType->describe(VerbosityLevel::precise())); + + $function = $reflectionProvider->getFunction(new Name('NativeUnionTypes\doFoo'), null); + $functionVariant = $function->getOnlyVariant(); + $functionReturnType = $functionVariant->getReturnType(); + $this->assertInstanceOf(UnionType::class, $functionReturnType); + $this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $functionReturnType->describe(VerbosityLevel::precise())); + + $functionParameterType = $functionVariant->getParameters()[0]->getType(); + $this->assertInstanceOf(UnionType::class, $functionParameterType); + $this->assertSame('bool|int', $functionParameterType->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 @@ + - */ -class Override extends C { -} diff --git a/tests/PHPStan/Reflection/data/IRouter.php b/tests/PHPStan/Reflection/data/IRouter.php new file mode 100644 index 0000000000..184e235b4e --- /dev/null +++ b/tests/PHPStan/Reflection/data/IRouter.php @@ -0,0 +1,11 @@ +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.0 + +namespace NativeMixedType; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public mixed $fooProp; + + public function doFoo(mixed $foo): mixed + { + assertType('mixed', $foo); + assertType('mixed', $this->fooProp); + } + +} + +class Bar +{ + +} + +function doFoo(mixed $foo): mixed +{ + assertType('mixed', $foo); +} + +function (Foo $foo): void { + assertType('mixed', $foo->fooProp); + assertType('mixed', $foo->doFoo(1)); + assertType('mixed', doFoo(1)); +}; + +function (): void { + $f = function (mixed $foo): mixed { + assertType('mixed', $foo); + }; + + assertType('void', $f(1)); +}; diff --git a/tests/PHPStan/Reflection/data/returns-by-reference-enum.php b/tests/PHPStan/Reflection/data/returns-by-reference-enum.php new file mode 100644 index 0000000000..02e0c40704 --- /dev/null +++ b/tests/PHPStan/Reflection/data/returns-by-reference-enum.php @@ -0,0 +1,11 @@ += 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 @@ += 8.0 + +namespace NativeStaticReturnType; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(): static + { + return new static(); + } + + public function doBar(): void + { + assertType('static(NativeStaticReturnType\Foo)', $this->doFoo()); + } + + /** + * @return callable(): static + */ + public function doBaz(): callable + { + $f = function (): static { + return new static(); + }; + + assertType('static(NativeStaticReturnType\Foo)', $f()); + + return $f; + } + +} + +class Bar extends Foo +{ + +} + +function (Foo $foo): void { + assertType('NativeStaticReturnType\Foo', $foo->doFoo()); + + $callable = $foo->doBaz(); + assertType('callable(): NativeStaticReturnType\Foo', $callable); + assertType('NativeStaticReturnType\Foo', $callable()); +}; + +function (Bar $bar): void { + assertType('NativeStaticReturnType\Bar', $bar->doFoo()); + + $callable = $bar->doBaz(); + assertType('callable(): NativeStaticReturnType\Bar', $callable); + assertType('NativeStaticReturnType\Bar', $callable()); +}; diff --git a/tests/PHPStan/Reflection/data/unionTypes.php b/tests/PHPStan/Reflection/data/unionTypes.php new file mode 100644 index 0000000000..e4b24b0acd --- /dev/null +++ b/tests/PHPStan/Reflection/data/unionTypes.php @@ -0,0 +1,93 @@ += 8.0 + +namespace NativeUnionTypes; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class Foo +{ + + public int|bool $fooProp; + + public function doFoo(int|bool $foo): self|Bar + { + assertType('bool|int', $foo); + assertType('bool|int', $this->fooProp); + assertNativeType('bool|int', $foo); + } + +} + +class Bar +{ + +} + +function doFoo(int|bool $foo): Foo|Bar +{ + assertType('bool|int', $foo); + assertNativeType('bool|int', $foo); +} + +function (Foo $foo): void { + assertType('bool|int', $foo->fooProp); + assertType('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $foo->doFoo(1)); + assertType('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', doFoo(1)); +}; + +function (): void { + $f = function (int|bool $foo): Foo|Bar { + assertType('bool|int', $foo); + }; + + assertType('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $f(1)); +}; + +class Baz +{ + + public function doFoo(array|false $foo): void + { + assertType('array|false', $foo); + assertNativeType('array|false', $foo); + assertType('array|false', $this->doBar()); + } + + public function doBar(): array|false + { + + } + + /** + * @param array $foo + */ + public function doBaz(array|false $foo): void + { + assertType('array|false', $foo); + assertNativeType('array|false', $foo); + + assertType('array|false', $this->doLorem()); + } + + /** + * @return array + */ + public function doLorem(): array|false + { + + } + + public function doIpsum(int|string|null $nullable): void + { + assertType('int|string|null', $nullable); + assertNativeType('int|string|null', $nullable); + assertType('int|string|null', $this->doDolor()); + } + + public function doDolor(): int|string|null + { + + } + +} diff --git a/tests/PHPStan/Rules/AlwaysFailRule.php b/tests/PHPStan/Rules/AlwaysFailRule.php index af3809d23f..95d3ba5b1a 100644 --- a/tests/PHPStan/Rules/AlwaysFailRule.php +++ b/tests/PHPStan/Rules/AlwaysFailRule.php @@ -4,11 +4,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use function count; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class AlwaysFailRule implements \PHPStan\Rules\Rule +class AlwaysFailRule implements Rule { public function getNodeType(): string @@ -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/ApiClassExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php new file mode 100644 index 0000000000..26651b4e97 --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ApiClassExtendsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiClassExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/class-extends-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-extends-out-of-phpstan.php'], [ + [ + 'Extending PHPStan\Type\FileTypeMapper is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + 9, + $tip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php new file mode 100644 index 0000000000..e14d95f33e --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -0,0 +1,66 @@ + + */ +class ApiClassImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiClassImplementsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/class-implements-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-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.', + 20, + $tip, + ], + [ + 'Implementing PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 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/ApiInstantiationRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php new file mode 100644 index 0000000000..0d96c5a664 --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php @@ -0,0 +1,53 @@ + + */ +class ApiInstantiationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiInstantiationRule( + new ApiRuleHelper(), + $this->createReflectionProvider(), + ); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/new-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/new-out-of-phpstan.php'], [ + [ + 'Creating new PHPStan\Type\FileTypeMapper is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + 18, + $tip, + ], + [ + 'Creating new PHPStan\DependencyInjection\NeonAdapter is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', + 19, + $tip, + ], + [ + 'Creating new PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType 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/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php new file mode 100644 index 0000000000..770c59da3a --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php @@ -0,0 +1,56 @@ + + */ +class ApiInterfaceExtendsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiInterfaceExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/interface-extends-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/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.', + 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/ApiMethodCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php new file mode 100644 index 0000000000..d91f116610 --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ApiMethodCallRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiMethodCallRule(new ApiRuleHelper()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/method-call-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/method-call-out-of-phpstan.php'], [ + [ + 'Calling PHPStan\Rules\Debug\DumpTypeRule::getNodeType() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', + 17, + $tip, + ], + [ + 'Calling PHPStan\Type\Php\ArrayKeyDynamicReturnTypeExtension::isFunctionSupported() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', + 27, + $tip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php new file mode 100644 index 0000000000..93d8428b77 --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php @@ -0,0 +1,154 @@ +createMock(Scope::class); + $scope->method('getNamespace')->willReturn($scopeNamespace); + $scope->method('getFile')->willReturn($scopeFile); + $this->assertSame($expected, $rule->isPhpStanCode($scope, $nameToCheck, $declaringFileNameToCheck)); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php new file mode 100644 index 0000000000..8331674e4b --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ApiStaticCallRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiStaticCallRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/static-call-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/static-call-out-of-phpstan.php'], [ + [ + 'Calling PHPStan\Command\CommandHelper::begin() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', + 17, + $tip, + ], + [ + 'Calling PHPStan\Node\InClassNode::__construct() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', + 33, + $tip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php new file mode 100644 index 0000000000..da2dbbeefe --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ApiTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ApiTraitUseRule(new ApiRuleHelper(), $this->createReflectionProvider()); + } + + public function testRuleInPhpStan(): void + { + $this->analyse([__DIR__ . '/data/trait-use-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/trait-use-out-of-phpstan.php'], [ + [ + 'Using PHPStan\Type\JustNullableTypeTrait is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.', + 15, + $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..b811039ec7 --- /dev/null +++ b/tests/PHPStan/Rules/Api/NodeConnectingVisitorAttributesRuleTest.php @@ -0,0 +1,35 @@ + + */ +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.', + 22, + 'See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules', + ], + [ + 'Node attribute \'parent\' is no longer available.', + 24, + '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/PhpStanNamespaceIn3rdPartyPackageRuleTest.php b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php new file mode 100644 index 0000000000..4c50e26ccc --- /dev/null +++ b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php @@ -0,0 +1,62 @@ + + */ +class PhpStanNamespaceIn3rdPartyPackageRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PhpStanNamespaceIn3rdPartyPackageRule(new ApiRuleHelper()); + } + + protected function tearDown(): void + { + @unlink(__DIR__ . '/composer.json'); + } + + public function testRulePhpStanNamespaceInPhpStanPackage(): void + { + $this->createComposerJson('phpstan/foo'); + $this->analyse([__DIR__ . '/data/phpstan-namespace.php'], []); + } + + public function testRulePhpStanNamespaceIn3rdPartyPackage(): void + { + $this->createComposerJson('my/foo'); + $this->analyse([__DIR__ . '/data/phpstan-namespace.php'], [ + [ + 'Declaring PHPStan namespace is not allowed in 3rd party packages.', + 3, + "See:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + ], + ]); + } + + public function testRuleCustomNamespaceInPhpStanPackage(): void + { + $this->createComposerJson('phpstan/foo'); + $this->analyse([__DIR__ . '/data/custom-namespace.php'], []); + } + + public function testRuleCustomNamespaceIn3rdPartyPackage(): void + { + $this->createComposerJson('my/foo'); + $this->analyse([__DIR__ . '/data/custom-namespace.php'], []); + } + + private function createComposerJson(string $packageName): void + { + FileWriter::write(__DIR__ . '/composer.json', Json::encode(['name' => $packageName], Json::PRETTY)); + } + +} 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 @@ +getNodeType(); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/method-call-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/method-call-out-of-phpstan.php new file mode 100644 index 0000000000..6a7e171136 --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/method-call-out-of-phpstan.php @@ -0,0 +1,40 @@ +getNodeType(); // not allowed + } + + public function doBar(ConstantArrayType $arrayType): void + { + $arrayType->getKeyTypes(); // @api above ConstantArrayType + } + + public function doBaz(ArrayKeyDynamicReturnTypeExtension $ext, FunctionReflection $func): void + { + $ext->isFunctionSupported($func); // not allowed + } + + public function doLorem(FileTypeMapper $fileTypeMapper): void + { + $fileTypeMapper->getResolvedPhpDoc('foo', null, null, null, '/** */'); // @api above method + } + + public function doIpsum(TemplateType $type): void + { + echo $type->getName(); // @api above interface + } + +} diff --git a/tests/PHPStan/Rules/Api/data/new-in-phpstan.php b/tests/PHPStan/Rules/Api/data/new-in-phpstan.php new file mode 100644 index 0000000000..5b7ceaef3e --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/new-in-phpstan.php @@ -0,0 +1,26 @@ +getAttribute("parent"); + $custom = $node->getAttribute("myCustomAttribute"); + $parent = $node->getAttribute($this->attrName); + + 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 + ScopeContext::create(__DIR__ . '/test.php'); // @api above method + } + +} + +class Bar extends InClassNode +{ + + public function __construct() + { + parent::__construct(); + } + +} + +class Baz extends ObjectType +{ + + public function __construct() + { + parent::__construct(); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/trait-use-in-phpstan.php b/tests/PHPStan/Rules/Api/data/trait-use-in-phpstan.php new file mode 100644 index 0000000000..edca446f9f --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/trait-use-in-phpstan.php @@ -0,0 +1,12 @@ + - */ -class AppendedArrayItemTypeRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\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.', - 30, - ], - [ - 'Array (array) does not accept Closure(): int.', - 43, - ], - [ - 'Array (array) does not accept AppendedArrayItem\Baz.', - 77, - ], - ] - ); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php deleted file mode 100644 index 502df29bc4..0000000000 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class AppendedArrayKeyTypeRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\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, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php new file mode 100644 index 0000000000..3027ce23b7 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -0,0 +1,66 @@ + + */ +class ArrayDestructuringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true); + + return new ArrayDestructuringRule( + $ruleLevelHelper, + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, false, false), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/array-destructuring.php'], [ + [ + 'Cannot use array destructuring on array|null.', + 11, + ], + [ + 'Offset 0 does not exist on array{}.', + 12, + ], + [ + 'Cannot use array destructuring on stdClass.', + 13, + ], + [ + 'Offset 2 does not exist on array{1, 2}.', + 15, + ], + [ + 'Offset \'a\' does not exist on array{b: 1}.', + 22, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/array-destructuring-nullsafe.php'], [ + [ + 'Cannot use array destructuring on array|null.', + 10, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/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 d8b999e8e6..a99a20557c 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class DeadForeachRuleTest extends RuleTestCase { @@ -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 28e7898337..6d775a2f7c 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -2,16 +2,22 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use function define; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DuplicateKeysInLiteralArraysRuleTest extends \PHPStan\Testing\RuleTestCase +class DuplicateKeysInLiteralArraysRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DuplicateKeysInLiteralArraysRule( - new \PhpParser\PrettyPrinter\Standard() + new ExprPrinter(new Printer()), ); } @@ -39,6 +45,34 @@ 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, + ], + [ + 'Array has 2 duplicate keys with value \'foo\' (\'foo\', $key).', + 102, + ], + [ + 'Array has 2 duplicate keys with value \'bar\' (\'bar\', $key).', + 103, + ], + [ + 'Array has 2 duplicate keys with value \'key\' (\'key\', $key2).', + 105, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index e1cc3685d0..757b25cbd7 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -2,15 +2,21 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidKeyInArrayDimFetchRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + 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 @@ -32,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 7bb10e9e47..7a40122d1c 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -2,13 +2,17 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidKeyInArrayItemRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidKeyInArrayItemRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidKeyInArrayItemRule(true); } @@ -59,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 2c64be2e26..c907f46ddd 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -2,17 +2,26 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IterableInForeachRuleTest extends \PHPStan\Testing\RuleTestCase +class IterableInForeachRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule { - return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)); } public function testCheckWithMaybes(): void @@ -23,14 +32,116 @@ 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, ], [ 'Iterating over an object of an unknown class IterablesInForeach\Bar.', 47, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug5744(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5744.php'], [ + /*[ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 15, + ],*/ + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 28, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/foreach-iterable-nullsafe.php'], [ + [ + 'Argument of an invalid type array|null supplied for foreach, only iterables are supported.', + 14, ], ]); } + public function testBug6564(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6564.php'], []); + } + + 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 9d315091bd..66374feb50 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -2,19 +2,33 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NonexistentOffsetInArrayDimFetchRuleTest extends \PHPStan\Testing\RuleTestCase +class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + 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, $this->checkImplicitMixed, false, true); + return new NonexistentOffsetInArrayDimFetchRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), - true + $ruleLevelHelper, + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), + true, ); } @@ -22,24 +36,26 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [ [ - 'Offset \'b\' does not exist on array(\'a\' => stdClass, 0 => 2).', + 'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.', 17, ], [ - 'Offset 1 does not exist on array(\'a\' => stdClass, 0 => 2).', + 'Offset 1 does not exist on array{a: stdClass, 0: 2}.', 18, ], [ - 'Offset \'a\' does not exist on array(\'b\' => 1).', + 'Offset \'a\' does not exist on array{b: 1}.', 55, ], [ 'Access to offset \'bar\' on an unknown class NonexistentOffset\Bar.', 101, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an offset on an unknown class NonexistentOffset\Bar.', 102, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Offset 0 does not exist on array.', @@ -74,41 +90,33 @@ 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, ], [ - 'Offset \'b\' does not exist on array(\'a\' => \'blabla\').', + 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 225, ], [ - 'Offset \'b\' does not exist on array(\'a\' => \'blabla\').', + 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 228, ], [ - 'Offset string does not exist on array.', - 240, - ], - [ - 'Cannot access offset \'a\' on Closure(): mixed.', + 'Cannot access offset \'a\' on Closure(): void.', 253, ], [ - 'Cannot access offset \'a\' on array(\'a\' => 1, \'b\' => 1)|(Closure(): mixed).', + '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, @@ -118,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, ], [ @@ -153,6 +161,14 @@ public function testRule(): void 'Cannot access offset \'foo\' on array|int.', 443, ], + [ + '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, + ], ]); } @@ -168,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, ], ]); @@ -182,7 +198,7 @@ public function testAssignOp(): void { $this->analyse([__DIR__ . '/data/offset-access-assignop.php'], [ [ - 'Offset \'foo\' does not exist on array().', + 'Offset \'foo\' does not exist on array{}.', 4, ], [ @@ -222,10 +238,706 @@ 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'], []); } + public function testIntersection(): void + { + $this->analyse([__DIR__ . '/data/nonexistent-offset-intersection.php'], []); + } + + public function testBug3782(): void + { + $this->analyse([__DIR__ . '/data/bug-3782.php'], [ + [ + 'Cannot access offset (int|string) on $this(Bug3782\HelloWorld)|(ArrayAccess&Bug3782\HelloWorld).', + 11, + ], + ]); + } + + public function testBug4432(): void + { + $this->analyse([__DIR__ . '/data/bug-4432.php'], []); + } + + public function testBug1664(): void + { + $this->analyse([__DIR__ . '/data/bug-1664.php'], []); + } + + public function testBug2689(): void + { + $this->analyse([__DIR__ . '/data/bug-2689.php'], [ + [ + 'Cannot access an offset on callable.', + 14, + ], + ]); + } + + public function testBug5169(): void + { + $this->analyse([__DIR__ . '/data/bug-5169.php'], [ + [ + 'Cannot access offset mixed on (float|int).', + 29, + ], + ]); + } + + public function testBug3297(): void + { + $this->analyse([__DIR__ . '/data/bug-3297.php'], []); + } + + public function testBug4829(): void + { + $this->analyse([__DIR__ . '/data/bug-4829.php'], []); + } + + public function testBug3784(): void + { + $this->analyse([__DIR__ . '/data/bug-3784.php'], []); + } + + public function testBug3700(): void + { + $this->analyse([__DIR__ . '/data/bug-3700.php'], []); + } + + public function testBug4842(): void + { + $this->analyse([__DIR__ . '/data/bug-4842.php'], []); + } + + public function testBug5669(): void + { + $this->analyse([__DIR__ . '/data/bug-5669.php'], [ + [ + 'Access to offset \'%customer…\' on an unknown class Bug5669\arr.', + 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug5744(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5744.php'], [ + [ + 'Cannot access offset \'permission\' on mixed.', + 16, + ], + [ + 'Cannot access offset \'permission\' on mixed.', + 29, + ], + [ + 'Cannot access offset \'permission\' on mixed.', + 39, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/nonexistent-offset-nullsafe.php'], [ + [ + 'Offset 1 does not exist on array{a: int}.', + 18, + ], + ]); + } + + public function testBug4926(): void + { + $this->analyse([__DIR__ . '/data/bug-4926.php'], []); + } + + public function testBug3171(): void + { + $this->analyse([__DIR__ . '/data/bug-3171.php'], []); + } + + 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 testBug11390(): void + { + $this->analyse([__DIR__ . '/data/bug-11390.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 624b446d91..e131c311a4 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php @@ -4,19 +4,20 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class OffsetAccessAssignOpRuleTest extends \PHPStan\Testing\RuleTestCase +class OffsetAccessAssignOpRuleTest extends RuleTestCase { - /** @var bool */ - private $checkUnions; + private bool $checkUnions; 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); } @@ -37,4 +38,14 @@ public function testRuleWithoutUnions(): void $this->analyse([__DIR__ . '/data/offset-access-assignop.php'], []); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkUnions = true; + $this->analyse([__DIR__ . '/data/offset-access-assignop-nullsafe.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index 30ef2433d6..533dd185ae 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -2,20 +2,22 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class OffsetAccessAssignmentRuleTest extends \PHPStan\Testing\RuleTestCase +class OffsetAccessAssignmentRuleTest extends RuleTestCase { - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false, false, false, true); return new OffsetAccessAssignmentRule($ruleLevelHelper); } @@ -58,7 +60,7 @@ public function testOffsetAccessAssignmentToScalar(): void 68, ], [ - 'Cannot assign offset array(1, 2, 3) to SplObjectStorage.', + 'Cannot assign offset array{1, 2, 3} to SplObjectStorage.', 72, ], [ @@ -69,7 +71,7 @@ public function testOffsetAccessAssignmentToScalar(): void 'Cannot assign new offset to OffsetAccessAssignment\ObjectWithOffsetAccess.', 81, ], - ] + ], ); } @@ -100,7 +102,7 @@ public function testOffsetAccessAssignmentToScalarWithoutMaybes(): void 68, ], [ - 'Cannot assign offset array(1, 2, 3) to SplObjectStorage.', + 'Cannot assign offset array{1, 2, 3} to SplObjectStorage.', 72, ], [ @@ -111,7 +113,7 @@ public function testOffsetAccessAssignmentToScalarWithoutMaybes(): void 'Cannot assign new offset to OffsetAccessAssignment\ObjectWithOffsetAccess.', 81, ], - ] + ], ); } @@ -127,4 +129,70 @@ public function testAssignNewOffsetToStubbedClass(): void $this->analyse([__DIR__ . '/data/new-offset-stub.php'], []); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/offset-access-assignment-nullsafe.php'], [ + [ + 'Cannot assign offset int|null to string.', + 14, + ], + ]); + } + + 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 d7099e1931..ff9ae587d7 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -5,16 +5,17 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class 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 @@ -36,11 +37,42 @@ public function testRule(): void 'ArrayAccess does not accept array.', 21, ], + [ + 'ArrayAccess does not accept string.', + 24, + ], [ 'ArrayAccess does not accept float.', - 35, + 38, + ], + [ + 'ArrayAccess does not accept int.', + 58, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/offset-access-value-assignment-nullsafe.php'], [ + [ + 'ArrayAccess does not accept int|null.', + 18, ], ]); } + 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/OffsetAccessWithoutDimForReadingRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php index 027f8c420f..db6846cfa0 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class OffsetAccessWithoutDimForReadingRuleTest extends \PHPStan\Testing\RuleTestCase +class OffsetAccessWithoutDimForReadingRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new OffsetAccessWithoutDimForReadingRule(); } @@ -82,7 +85,7 @@ public function testOffsetAccessWithoutDimForReading(): void 'Cannot use [] for reading.', 30, ], - ] + ], ); } diff --git a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php index 833f4d6b78..15abfb7cef 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,23 +5,27 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ 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.', @@ -38,4 +42,78 @@ public function testRule(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/unpack-iterable-nullsafe.php'], [ + [ + 'Only iterables can be unpacked, array|null given.', + 17, + ], + ]); + } + + 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/appended-array-item.php b/tests/PHPStan/Rules/Arrays/data/appended-array-item.php index f6a45e866b..4d77299b03 100644 --- a/tests/PHPStan/Rules/Arrays/data/appended-array-item.php +++ b/tests/PHPStan/Rules/Arrays/data/appended-array-item.php @@ -23,6 +23,8 @@ public function doFoo() $this->callables[] = [__CLASS__, 'classMethod']; $world = 'world'; $this->callables[] = ['Foo', "Hello $world"]; + + $this->integers[] = &$world; } public function assignOp() diff --git a/tests/PHPStan/Rules/Arrays/data/appended-array-key.php b/tests/PHPStan/Rules/Arrays/data/appended-array-key.php index 3381fabab9..650e62e736 100644 --- a/tests/PHPStan/Rules/Arrays/data/appended-array-key.php +++ b/tests/PHPStan/Rules/Arrays/data/appended-array-key.php @@ -68,3 +68,21 @@ public function doBar() } } + +class MorePreciseKey +{ + + /** @var array<1|2|3, string> */ + private $test; + + public function doFoo(int $i): void + { + $this->test[$i] = 'foo'; + } + + public function doBar(): void + { + $this->test[4] = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php new file mode 100644 index 0000000000..d8389afe5e --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php @@ -0,0 +1,23 @@ += 8.0 + +namespace ArrayDestructuringNullsafe; + +class Foo +{ + + public function doFooBar(?Bar $bar): void + { + [$a] = $bar?->getArray(); + } + +} + +class Bar +{ + + public function getArray(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php new file mode 100644 index 0000000000..8dd4b625e0 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php @@ -0,0 +1,54 @@ + $a] = ['a' => 1]; + + ['a' => $a] = ['b' => 1]; + } + + public function doBaz(): void + { + $arrayObject = new FooArrayObject(); + ['a' => $a] = $arrayObject; + } + +} + +class FooArrayObject implements \ArrayAccess +{ + + public function offsetGet($key) + { + return true; + } + + public function offsetSet($key, $value): void + { + } + + public function offsetUnset($key): void + { + } + + public function offsetExists($key): bool + { + return false; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/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 @@ + $tags + * @param numeric-string $tagId + */ +function printTagName(array $tags, string $tagId): void +{ + // Adding the second `*` to either of the following lines makes the error disappear + + $tagsById = array_combine(array_column($tags, 'id'), $tags); + if (false !== $tagsById) { + echo $tagsById[$tagId]['tagName'] . PHP_EOL; + } +} + +printTagName( + [ + ['id' => '123', 'tagName' => 'abc'], + ['id' => '4.5', 'tagName' => 'def'], + ['id' => '6e78', 'tagName' => 'ghi'] + ], + '4.5' +); diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11447.php b/tests/PHPStan/Rules/Arrays/data/bug-11447.php new file mode 100644 index 0000000000..f59f2bdd6a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11447.php @@ -0,0 +1,8 @@ + $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-1664.php b/tests/PHPStan/Rules/Arrays/data/bug-1664.php new file mode 100644 index 0000000000..64fb492a76 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-1664.php @@ -0,0 +1,17 @@ + $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 @@ +listeners[$name][] = $callback; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-3171.php b/tests/PHPStan/Rules/Arrays/data/bug-3171.php new file mode 100644 index 0000000000..96505bbf6c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-3171.php @@ -0,0 +1,17 @@ + $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-3782.php b/tests/PHPStan/Rules/Arrays/data/bug-3782.php new file mode 100644 index 0000000000..e3871d9827 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-3782.php @@ -0,0 +1,14 @@ + $value){ + $this[$key] = $value; + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-3784.php b/tests/PHPStan/Rules/Arrays/data/bug-3784.php new file mode 100644 index 0000000000..349a1ec20e --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-3784.php @@ -0,0 +1,23 @@ + '', '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-4432.php b/tests/PHPStan/Rules/Arrays/data/bug-4432.php new file mode 100644 index 0000000000..8cc909faf0 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4432.php @@ -0,0 +1,18 @@ + 'blah1', 'cat2' => 'blah2', ?'cat3' => 'blah3'). + public function stan() : void + { + $ret = []; + + // if $result has 1 element, the error does not occur + // if $result is [], the error occurs + $result = ["val1", "val2"]; + + foreach ($result as $val) { + // if I replace $val with a string, the error does not occur + //$val = "test"; + + // if I remove one assignment, the error does not occur + $ret[$val]['cat1'] = "blah1"; + $ret[$val]['cat2'] = "blah2"; + $ret[$val]['cat3'] = 'blah3'; + + $t1 = $ret[$val]['cat1']; + $t2 = $ret[$val]['cat2']; + $t3 = $ret[$val]['cat3']; // error occurs here + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4842.php b/tests/PHPStan/Rules/Arrays/data/bug-4842.php new file mode 100644 index 0000000000..5230a347f9 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4842.php @@ -0,0 +1,31 @@ +mappings = $mappings; + } + + /** + * @param "21021200"|"asd" $code + */ + function foo(string $code): string + { + if (isset($this->mappings[$code])) { + return (string)$this->mappings[$code]; + } + + throw new \RuntimeException(); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4885.php b/tests/PHPStan/Rules/Arrays/data/bug-4885.php new file mode 100644 index 0000000000..8d3853dd77 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4885.php @@ -0,0 +1,13 @@ += 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-4926.php b/tests/PHPStan/Rules/Arrays/data/bug-4926.php new file mode 100644 index 0000000000..3e61b67f6b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4926.php @@ -0,0 +1,17 @@ +data['customer']['first_name'] ?? null; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5169.php b/tests/PHPStan/Rules/Arrays/data/bug-5169.php new file mode 100644 index 0000000000..71ab080671 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5169.php @@ -0,0 +1,31 @@ + $configs + * + * @return array + */ + protected function merge(array $configs): array + { + $result = []; + foreach ($configs as $config) { + $result += $config; + + foreach ($config as $name => $dto) { + $result[$name] += $dto; + } + } + + return $result; + } + + public function merge2($mixed): void + { + $f = $mixed - $mixed; + $f[$mixed] = true; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5372_2.php b/tests/PHPStan/Rules/Arrays/data/bug-5372_2.php new file mode 100644 index 0000000000..5e6caebd42 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5372_2.php @@ -0,0 +1,23 @@ + */ + private $map = []; + + /** + * @param array $values + */ + public function __construct(array $values) + { + assertType('array', $values); + foreach ($values as $v) { + assertType('non-empty-string', $v); + $this->map[$v] = 'whatever'; + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5447.php b/tests/PHPStan/Rules/Arrays/data/bug-5447.php new file mode 100644 index 0000000000..d758be05b5 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5447.php @@ -0,0 +1,36 @@ + + */ + private $parameters = []; + + /** + * @phpstan-param self::FIELD_* $key + */ + public function setParameter(string $key, string $value) : void + { + $this->parameters[$key] = $value; + } + + /** + * @phpstan-return array + */ + public function getParameters() : array + { + return $this->parameters; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-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-5669.php b/tests/PHPStan/Rules/Arrays/data/bug-5669.php new file mode 100644 index 0000000000..a280cce420 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5669.php @@ -0,0 +1,30 @@ + + */ + public function getReplacer() + { + return []; + } + +} + +class c extends a +{ + + public function getReplacer() + { + $replacer = parent::getReplacer(); + $replacer['%customer_salutation%'] = 'test'; + + return $replacer; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5744.php b/tests/PHPStan/Rules/Arrays/data/bug-5744.php new file mode 100644 index 0000000000..638911e179 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5744.php @@ -0,0 +1,43 @@ + $commandData){ + var_dump($commandData["permission"]); + } + } + } + + /** + * @phpstan-param mixed[] $plugin + */ + public function sayHello2(array $plugin): void + { + if(isset($plugin["commands"])){ + $pluginCommands = $plugin["commands"]; + foreach($pluginCommands as $commandName => $commandData){ + var_dump($commandData["permission"]); + } + } + } + + public function sayHello3(array $plugin): void + { + if(isset($plugin["commands"]) and is_array($plugin["commands"])){ + $pluginCommands = $plugin["commands"]; + foreach($pluginCommands as $commandName => $commandData){ + var_dump($commandData["permission"]); + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-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 @@ +isValueIterable()) { + /** @var mixed[] $value */ + foreach ($value as $_value) { + } + } + + + } + public function isValueIterable(): bool { + return true; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6605.php b/tests/PHPStan/Rules/Arrays/data/bug-6605.php new file mode 100644 index 0000000000..03df34d565 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6605.php @@ -0,0 +1,19 @@ + 'bar' + ]; + + $arr = ['a' => ['b' => [5]]]; + var_dump($arr['invalid']['c']); + var_dump($arr['a']['invalid']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-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, + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function doUnionKeys(string $key): void + { + $key2 = 'key'; + $a = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar', + 'key' => 'bar', + $key2 => 'foo', + ]; + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/foreach-iterable-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/foreach-iterable-nullsafe.php new file mode 100644 index 0000000000..4f19692219 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/foreach-iterable-nullsafe.php @@ -0,0 +1,17 @@ += 8.0 + +namespace IterablesInForeachNullsafe; + +class Foo +{ + + /** @var int[] */ + public array $array; +} + +function doFoo(?Foo $foo) +{ + foreach ($foo?->array as $x) { + // pass + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/foreach-mixed.php b/tests/PHPStan/Rules/Arrays/data/foreach-mixed.php new file mode 100644 index 0000000000..6a2a305fae --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/foreach-mixed.php @@ -0,0 +1,19 @@ += 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 + */ + private $fooA; + /** @var \ArrayAccess&iterable */ + private $fooB; + /** @var \ArrayAccess&\Countable */ + private $fooC; + /** @var \ArrayAccess&\stdClass */ + private $fooD; + + public function test(): void + { + $a = $this->fooA['bar']; + $b = $this->fooB['bar']; + $c = $this->fooC['bar']; + $d = $this->fooD['bar']; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php new file mode 100644 index 0000000000..8185c01f8f --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php @@ -0,0 +1,19 @@ += 8.0 + +namespace NonexistentOffsetNullsafe; + +class Foo +{ + + /** @var array{a: int} */ + public array $array = [ + 'a' => 1, + ]; + +} + +function nonexistentOffsetOnArray(?Foo $foo): void +{ + echo $foo?->array['a']; + echo $foo?->array[1]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php index 478696cdc2..4509a69a2b 100644 --- a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php @@ -444,3 +444,77 @@ public function doFoo(): void } } + +class Bug3282 +{ + /** + * @phpstan-param array{event: string, msg?: array{ts?: int}} $array + */ + public function foo(array $array): int + { + if (isset($array['msg']['ts'])) { + return 1; + } + + return 0; + } +} + +class MessageDescriptorTest +{ + + public function testDefinitions(): void + { + try { + doFoo(); + } catch (\TypeError $e) { + $trace = $e->getTrace(); + if (isset($trace[1]['args'][0])) { + $class = $trace[1]['args'][0]; + $this->fail(sprintf('Invalid phpDoc in class: %s', $class)); + } + + throw $e; + } + } + + /** @param array|null $array */ + function test($array): void { + var_dump($array['test1']['test2'] ?? true); + var_dump($array['test1'] ?? true); + } + +} + +/** + * @phpstan-type Version array{version: string, commit: string|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null} + */ +class VersionGuesser +{ + /** + * @param array $versionData + * + * @phpstan-param Version $versionData + * + * @return array + * @phpstan-return Version + */ + private function postprocess(array $versionData): array + { + if (!empty($versionData['feature_version']) && $versionData['feature_version'] === $versionData['version'] && $versionData['feature_pretty_version'] === $versionData['pretty_version']) { + unset($versionData['feature_version'], $versionData['feature_pretty_version']); + } + + return $versionData; + } +} + +class OnBool +{ + + public function doFoo(bool $b) + { + $b['foo'] = 1; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-nullsafe.php new file mode 100644 index 0000000000..3ddac9f8c8 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-nullsafe.php @@ -0,0 +1,15 @@ += 8.0 +declare(strict_types = 1); + +namespace OffsetAccessAssignmentNullsafe; + +class Bar +{ + public int $val; +} + +function doFoo(?Bar $bar) +{ + $str = 'abcd'; + $str[$bar?->val] = 'ok'; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php index 3818fd4ceb..a4723578fe 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php @@ -90,7 +90,7 @@ class ObjectWithOffsetAccess implements \ArrayAccess * @param string $offset * @return bool */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return true; } @@ -99,6 +99,7 @@ public function offsetExists($offset) * @param string $offset * @return int */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return 0; @@ -109,7 +110,7 @@ public function offsetGet($offset) * @param int $value * @return void */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { } @@ -117,7 +118,7 @@ public function offsetSet($offset, $value) * @param string $offset * @return void */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { } diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-assignop-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/offset-access-assignop-nullsafe.php new file mode 100644 index 0000000000..8b81b5d9aa --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-assignop-nullsafe.php @@ -0,0 +1,22 @@ += 8.0 +declare(strict_types=1); + +namespace OffsetAccessAssignOpNullsafe; + +class Bar +{ + public const INDEX = 'b'; + + /** @phpstan-var Bar::INDEX */ + public string $index = self::INDEX; +} + +function doFoo(?Bar $bar) +{ + /** @var array $array */ + $array = [ + 'a' => 123, + ]; + + $array['b'] += 'str'; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-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/offset-access-value-assignment-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-nullsafe.php new file mode 100644 index 0000000000..16e6d3cb00 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-nullsafe.php @@ -0,0 +1,19 @@ += 8.0 +declare(strict_types = 1); + +namespace OffsetAccessValueAssignmentNullsafe; + +class Bar +{ + public int $val; +} + +function doFoo(?Bar $bar) +{ + /** @var \ArrayAccess $array */ + $array = [ + 'a' => 123, + ]; + + $array['a'] = $bar?->val; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php index 2aa6760f87..2e4f00d7e3 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php @@ -19,6 +19,9 @@ public function doFoo(\ArrayAccess $arrayAccess): void $arrayAccess[] = 'baz'; $arrayAccess[] = ['foo']; + + $s = 'foo'; + $arrayAccess[] = &$s; } public function doBar(int $test): void @@ -41,3 +44,18 @@ public function doLorem(string $str): void } } + +class AppendToArrayAccess +{ + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/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-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/unpack-iterable-nullsafe.php new file mode 100644 index 0000000000..c4b49f2b75 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/unpack-iterable-nullsafe.php @@ -0,0 +1,21 @@ += 8.0 + +namespace UnpackIterableNullsafe; + +class Bar +{ + /** @var int[] */ + public array $array; +} + +class Foo +{ + + public function doFoo(?Bar $bar) + { + $foo = [ + ...$bar?->array, + ]; + } + +} diff --git a/tests/PHPStan/Rules/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 9161caf172..f08a205a79 100644 --- a/tests/PHPStan/Rules/Cast/EchoRuleTest.php +++ b/tests/PHPStan/Rules/Cast/EchoRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class EchoRuleTest extends RuleTestCase { @@ -15,7 +16,7 @@ class EchoRuleTest extends RuleTestCase protected function getRule(): Rule { return new EchoRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -23,7 +24,7 @@ public function testEchoRule(): void { $this->analyse([__DIR__ . '/data/echo.php'], [ [ - 'Parameter #1 (array()) of echo cannot be converted to string.', + 'Parameter #1 (array{}) of echo cannot be converted to string.', 7, ], [ @@ -31,7 +32,7 @@ public function testEchoRule(): void 9, ], [ - 'Parameter #1 (array()) of echo cannot be converted to string.', + 'Parameter #1 (array{}) of echo cannot be converted to string.', 11, ], [ @@ -39,13 +40,31 @@ public function testEchoRule(): void 11, ], [ - 'Parameter #1 (Closure(): mixed) of echo cannot be converted to string.', + 'Parameter #1 (Closure(): void) of echo cannot be converted to string.', 13, ], [ - 'Parameter #1 (\'string\'|array(\'string\')) of echo cannot be converted to string.', + 'Parameter #1 (\'string\'|array{\'string\'}) of echo cannot be converted to string.', 17, ], + [ + 'Parameter #1 (array{}) of echo cannot be converted to string.', + 29, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/echo-nullsafe.php'], [ + [ + 'Parameter #1 (array|null) of echo cannot be converted to string.', + 15, + ], ]); } diff --git a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index e0a0fd1e57..e5d5f11c2a 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -2,18 +2,27 @@ namespace PHPStan\Rules\Cast; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidCastRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidCastRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + 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 @@ -23,14 +32,6 @@ public function testRule(): void 'Cannot cast stdClass to string.', 7, ], - [ - 'Cannot cast array() to int.', - 16, - ], - [ - 'Cannot cast \'blabla\' to int.', - 21, - ], [ 'Cannot cast stdClass to int.', 23, @@ -39,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, @@ -50,4 +55,121 @@ public function testRule(): void ]); } + public function testBug5162(): void + { + $this->analyse([__DIR__ . '/data/bug-5162.php'], []); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-cast-nullsafe.php'], [ + [ + 'Cannot cast stdClass|null to string.', + 13, + ], + ]); + } + + 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 0429ec48ee..642b296e4f 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -2,19 +2,24 @@ namespace PHPStan\Rules\Cast; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidPartOfEncapsedStringRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidPartOfEncapsedStringRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( - new \PhpParser\PrettyPrinter\Standard(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new ExprPrinter(new Printer()), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -28,4 +33,18 @@ public function testRule(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-encapsed-part-nullsafe.php'], [ + [ + 'Part $bar?->obj (stdClass|null) of encapsed string cannot be cast to string.', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/PrintRuleTest.php b/tests/PHPStan/Rules/Cast/PrintRuleTest.php index 0671b278bd..fb0e7991fd 100644 --- a/tests/PHPStan/Rules/Cast/PrintRuleTest.php +++ b/tests/PHPStan/Rules/Cast/PrintRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class PrintRuleTest extends RuleTestCase { @@ -15,7 +16,7 @@ class PrintRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -23,7 +24,7 @@ public function testPrintRule(): void { $this->analyse([__DIR__ . '/data/print.php'], [ [ - 'Parameter array() of print cannot be converted to string.', + 'Parameter array{} of print cannot be converted to string.', 5, ], [ @@ -31,11 +32,11 @@ public function testPrintRule(): void 7, ], [ - 'Parameter Closure(): mixed of print cannot be converted to string.', + 'Parameter Closure(): void of print cannot be converted to string.', 9, ], [ - 'Parameter array() of print cannot be converted to string.', + 'Parameter array{} of print cannot be converted to string.', 13, ], [ @@ -43,14 +44,28 @@ public function testPrintRule(): void 15, ], [ - 'Parameter Closure(): mixed of print cannot be converted to string.', + 'Parameter Closure(): void of print cannot be converted to string.', 17, ], [ - 'Parameter \'string\'|array(\'string\') of print cannot be converted to string.', + 'Parameter \'string\'|array{\'string\'} of print cannot be converted to string.', 21, ], ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/print-nullsafe.php'], [ + [ + 'Parameter array|null of print cannot be converted to string.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php new file mode 100644 index 0000000000..ebab5c0aa6 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php @@ -0,0 +1,51 @@ + + */ +class UnsetCastRuleTest extends RuleTestCase +{ + + private int $phpVersion; + + protected function getRule(): Rule + { + return new UnsetCastRule(new PhpVersion($this->phpVersion)); + } + + public function dataRule(): array + { + return [ + [ + 70400, + [], + ], + [ + 80000, + [ + [ + 'The (unset) cast is no longer supported in PHP 8.0 and later.', + 6, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param list $errors + */ + public function testRule(int $phpVersion, array $errors): void + { + $this->phpVersion = $phpVersion; + $this->analyse([__DIR__ . '/data/unset-cast.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Cast/data/bug-5162.php b/tests/PHPStan/Rules/Cast/data/bug-5162.php new file mode 100644 index 0000000000..1b38c65ce9 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/bug-5162.php @@ -0,0 +1,58 @@ +> + */ + function Get() + { + switch ( rand(1,3) ) + { + case 1: + return ['a' => 'val']; + case 2: + return ['a' => '1']; + case 3: + return ['a' => []]; + } + return []; + } + + public function doFoo(): void + { + // This variant works + $result1 = $this->Get(); + if ( ! array_key_exists('a', $result1)) + { + exit(1); + } + if ( ! is_numeric( $result1['a'] ) ) + { + exit(1); + } + $val = (float) $result1['a']; + } + + public function doBar(): void + { + // This variant doesn't work .. but is logically identical + $result2 = $this->Get(); + if ( array_key_exists('a',$result2) && ! is_numeric( $result2['a'] ) ) + { + exit(1); + } + if ( ! array_key_exists('a', $result2) ) + { + exit(1); + } + $val = (float) $result2['a']; + } + +} diff --git a/tests/PHPStan/Rules/Cast/data/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 + +declare(strict_types = 1); + +namespace EchoNullsafe; + +class Bar +{ + /** @var int[] */ + public array $array; +} + +function def(?Bar $bar) +{ + echo $bar?->array; +} diff --git a/tests/PHPStan/Rules/Cast/data/echo.php b/tests/PHPStan/Rules/Cast/data/echo.php index 0235559328..a8f0b53da0 100644 --- a/tests/PHPStan/Rules/Cast/data/echo.php +++ b/tests/PHPStan/Rules/Cast/data/echo.php @@ -23,3 +23,9 @@ function (array $test) /** @var string $test */ echo $test; }; + +function (): void { + { + echo []; + } +}; diff --git a/tests/PHPStan/Rules/Cast/data/invalid-cast-nullsafe.php b/tests/PHPStan/Rules/Cast/data/invalid-cast-nullsafe.php new file mode 100644 index 0000000000..ccdd3b4a61 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/invalid-cast-nullsafe.php @@ -0,0 +1,14 @@ += 8.0 + +namespace InvalidCastNullsafe; + +class Bar +{ + public \stdClass $obj; +} + +function doFoo( + ?Bar $bar +) { + (string) $bar?->obj; +}; diff --git a/tests/PHPStan/Rules/Cast/data/invalid-cast.php b/tests/PHPStan/Rules/Cast/data/invalid-cast.php index 809b530e71..4c48e6acfa 100644 --- a/tests/PHPStan/Rules/Cast/data/invalid-cast.php +++ b/tests/PHPStan/Rules/Cast/data/invalid-cast.php @@ -57,3 +57,13 @@ function ( (string) $xml; (bool) $xml; }; + +function (): void { + $ch = curl_init(); + (int) $ch; +}; + +function (): void { + $ch = curl_multi_init(); + (int) $ch; +}; diff --git a/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-nullsafe.php b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-nullsafe.php new file mode 100644 index 0000000000..25f93d4fd1 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-nullsafe.php @@ -0,0 +1,12 @@ += 8.0 + +namespace InvalidEncapsedPartNullsafe; + +class Bar +{ + public \stdClass $obj; +} + +function doFoo(?Bar $bar) { + "{$bar?->obj} bar"; +} diff --git a/tests/PHPStan/Rules/Cast/data/mixed-cast.php b/tests/PHPStan/Rules/Cast/data/mixed-cast.php new file mode 100644 index 0000000000..73085a755e --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/mixed-cast.php @@ -0,0 +1,31 @@ += 8.0 + +namespace MixedCast; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump((int) $t); + var_dump((bool) $t); + var_dump((float) $t); + var_dump((string) $t); + var_dump((array) $t); + var_dump((object) $t); + + var_dump((int) $explicit); + var_dump((bool) $explicit); + var_dump((float) $explicit); + var_dump((string) $explicit); + var_dump((array) $explicit); + var_dump((object) $explicit); + + var_dump((int) $implicit); + var_dump((bool) $implicit); + var_dump((float) $implicit); + var_dump((string) $implicit); + var_dump((array) $implicit); + var_dump((object) $implicit); +} diff --git a/tests/PHPStan/Rules/Cast/data/print-nullsafe.php b/tests/PHPStan/Rules/Cast/data/print-nullsafe.php new file mode 100644 index 0000000000..444692e425 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/print-nullsafe.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace PrintNullsafe; + +class Bar +{ + /** @var int[] */ + public array $array; +} + +function def(?Bar $bar) +{ + print $bar?->array; +} diff --git a/tests/PHPStan/Rules/Cast/data/unset-cast.php b/tests/PHPStan/Rules/Cast/data/unset-cast.php new file mode 100644 index 0000000000..855685fe2c --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/unset-cast.php @@ -0,0 +1,7 @@ + + */ +class AccessPrivateConstantThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AccessPrivateConstantThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/access-private-constant-static.php'], [ + [ + 'Unsafe access to private constant AccessPrivateConstantThroughStatic\Foo::FOO through static::.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/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 new file mode 100644 index 0000000000..09ab4668c8 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -0,0 +1,194 @@ + + */ +class ClassAttributesRuleTest extends RuleTestCase +{ + + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new ClassAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/class-attributes.php'], [ + [ + 'Attribute class ClassAttributes\Nonexistent does not exist.', + 22, + ], + [ + 'Class ClassAttributes\Foo is not an Attribute class.', + 28, + ], + [ + 'Class ClassAttributes\Bar referenced with incorrect case: ClassAttributes\baR.', + 34, + ], + [ + 'Attribute class ClassAttributes\Baz does not have the class target.', + 46, + ], + [ + 'Attribute class ClassAttributes\Bar is not repeatable but is already present above the class.', + 59, + ], + [ + 'Attribute class self does not exist.', + 65, + ], + [ + 'Attribute class ClassAttributes\AbstractAttribute is abstract.', + 77, + ], + [ + 'Attribute class ClassAttributes\Bar does not have a constructor and must be instantiated without any parameters.', + 83, + ], + [ + 'Constructor of attribute class ClassAttributes\NonPublicConstructor is not public.', + 100, + ], + [ + 'Attribute class ClassAttributes\AttributeWithConstructor constructor invoked with 0 parameters, 2 required.', + 118, + ], + [ + 'Attribute class ClassAttributes\AttributeWithConstructor constructor invoked with 1 parameter, 2 required.', + 119, + ], + [ + 'Unknown parameter $r in call to ClassAttributes\AttributeWithConstructor constructor.', + 120, + ], + [ + 'Interface ClassAttributes\InterfaceAsAttribute is not an Attribute class.', + 132, + ], + [ + 'Trait ClassAttributes\TraitAsAttribute is not an Attribute class.', + 142, + ], + [ + 'Attribute class ClassAttributes\FlagsAttributeWithPropertyTarget does not have the class target.', + 164, + ], + ]); + } + + public function testRuleForEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-attributes.php'], [ + [ + 'Attribute class EnumAttributes\AttributeWithPropertyTarget does not have the class target.', + 23, + ], + [ + 'Enum EnumAttributes\EnumAsAttribute is not an Attribute class.', + 35, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..93190d705a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -0,0 +1,60 @@ + + */ +class ClassConstantAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new ClassConstantAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/class-constant-attributes.php'], [ + [ + 'Attribute class ClassConstantAttributes\Foo does not have the class constant target.', + 26, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 25d480e13d..7fda6784cf 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -2,24 +2,42 @@ namespace PHPStan\Rules\Classes; +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; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ClassConstantRuleTest extends \PHPStan\Testing\RuleTestCase +class ClassConstantRuleTest extends RuleTestCase { + private int $phpVersion; + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker)); + $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 { + $this->phpVersion = PHP_VERSION_ID; $this->analyse( [ __DIR__ . '/data/class-constant.php', @@ -29,6 +47,7 @@ public function testClassConstant(): void [ 'Class ClassConstantNamespace\Bar not found.', 6, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Using self outside of class scope.', @@ -53,6 +72,7 @@ public function testClassConstant(): void [ 'Access to constant FOO on an unknown class ClassConstantNamespace\UnknownClass.', 21, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class ClassConstantNamespace\Foo referenced with incorrect case: ClassConstantNamespace\FOO.', @@ -74,15 +94,13 @@ public function testClassConstant(): void 'Access to undefined constant ClassConstantNamespace\Foo|string::DOLOR.', 33, ], - ] + ], ); } 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'], [ [ 'Access to private constant PRIVATE_BAR of class ClassConstantVisibility\Bar.', @@ -123,40 +141,342 @@ public function testClassConstantVisibility(): void [ 'Access to constant FOO on an unknown class ClassConstantVisibility\UnknownClassFirst.', 112, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to constant FOO on an unknown class ClassConstantVisibility\UnknownClassSecond.', 112, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Cannot access constant FOO on int|string.', 116, ], + [ + 'Access to undefined constant static(ClassConstantVisibility\AccessWithStatic)::BAR.', + 129, + ], [ 'Class ClassConstantVisibility\Foo referenced with incorrect case: ClassConstantVisibility\FOO.', - 122, + 135, ], [ 'Access to private constant PRIVATE_FOO of class ClassConstantVisibility\Foo.', - 122, + 135, ], ]); } public function testClassExists(): void { + $this->phpVersion = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/class-exists.php'], [ [ 'Class UnknownClass\Bar not found.', 24, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class UnknownClass\Foo not found.', 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class UnknownClass\Foo not found.', 29, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function dataClassConstantOnExpression(): array + { + return [ + [ + 70400, + [ + [ + 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', + 15, + ], + [ + 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', + 16, + ], + [ + 'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.', + 17, + ], + [ + '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, + ], + ], + ], + [ + 80000, + [ + [ + 'Accessing ::class constant on a dynamic string is not supported in PHP.', + 16, + ], + [ + 'Cannot access constant class on stdClass|null.', + 17, + ], + [ + 'Cannot access constant class on string|null.', + 18, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataClassConstantOnExpression + * @param list $errors + */ + public function testClassConstantOnExpression(int $phpVersion, array $errors): void + { + $this->phpVersion = $phpVersion; + $this->analyse([__DIR__ . '/data/class-constant-on-expr.php'], $errors); + } + + public function testAttributes(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-constant-attribute.php'], [ + [ + 'Access to undefined constant ClassConstantAttribute\Foo::BAR.', + 5, + ], + [ + 'Access to undefined constant ClassConstantAttribute\Foo::BAR.', + 9, + ], + [ + 'Access to undefined constant ClassConstantAttribute\Foo::BAR.', + 12, + ], + [ + 'Access to undefined constant ClassConstantAttribute\Foo::BAR.', + 15, + ], + [ + 'Access to undefined constant ClassConstantAttribute\Foo::BAR.', + 17, + ], + [ + 'Access to private constant FOO of class ClassConstantAttribute\Foo.', + 26, + ], + [ + 'Access to undefined constant ClassConstantAttribute\Foo::BAR.', + 26, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-constant-nullsafe.php'], []); + } + + 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 new file mode 100644 index 0000000000..5390b57ac9 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php @@ -0,0 +1,91 @@ + + */ +class DuplicateDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DuplicateDeclarationRule(); + } + + public function testDuplicateDeclarations(): void + { + $this->analyse( + [ + __DIR__ . '/data/duplicate-declarations.php', + ], + [ + [ + 'Cannot redeclare constant DuplicateDeclarations\Foo::CONST1.', + 8, + ], + [ + 'Cannot redeclare constant DuplicateDeclarations\Foo::CONST2.', + 10, + ], + [ + 'Cannot redeclare property DuplicateDeclarations\Foo::$prop1.', + 17, + ], + [ + 'Cannot redeclare property DuplicateDeclarations\Foo::$prop2.', + 20, + ], + [ + 'Cannot redeclare method DuplicateDeclarations\Foo::func1().', + 27, + ], + [ + 'Cannot redeclare method DuplicateDeclarations\Foo::Func1().', + 35, + ], + ], + ); + } + + public function testDuplicatePromotedProperty(): void + { + $this->analyse([__DIR__ . '/data/duplicate-promoted-property.php'], [ + [ + 'Cannot redeclare property DuplicatedPromotedProperty\Foo::$foo.', + 11, + ], + [ + 'Cannot redeclare property DuplicatedPromotedProperty\Foo::$bar.', + 13, + ], + ]); + } + + public function testDuplicateEnumCase(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/duplicate-enum-cases.php'], [ + [ + 'Cannot redeclare enum case DuplicatedEnumCase\Foo::BAR.', + 10, + ], + [ + 'Cannot redeclare enum case DuplicatedEnumCase\Boo::BAR.', + 17, + ], + [ + 'Cannot redeclare constant DuplicatedEnumCase\Hoo::BAR.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php new file mode 100644 index 0000000000..af02a79635 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -0,0 +1,153 @@ + + */ +class EnumSanityRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumSanityRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $expected = [ + /*[ + // reported by AbstractMethodInNonAbstractClassRule + 'Enum EnumSanity\EnumWithAbstractMethod contains abstract method foo().', + 7, + ],*/ + [ + 'Enum EnumSanity\EnumWithConstructorAndDestructor contains constructor.', + 12, + ], + [ + 'Enum EnumSanity\EnumWithConstructorAndDestructor contains destructor.', + 15, + ], + [ + 'Enum EnumSanity\EnumWithMagicMethods contains magic method __get().', + 21, + ], + [ + 'Enum EnumSanity\EnumWithMagicMethods contains magic method __set().', + 30, + ], + [ + 'Enum EnumSanity\PureEnumCannotRedeclareMethods cannot redeclare native method cases().', + 39, + ], + [ + 'Enum EnumSanity\BackedEnumCannotRedeclareMethods cannot redeclare native method cases().', + 54, + ], + [ + 'Enum EnumSanity\BackedEnumCannotRedeclareMethods cannot redeclare native method tryFrom().', + 58, + ], + [ + 'Enum EnumSanity\BackedEnumCannotRedeclareMethods cannot redeclare native method from().', + 62, + ], + [ + 'Backed enum EnumSanity\BackedEnumWithFloatType can have only "int" or "string" type.', + 67, + ], + [ + 'Backed enum EnumSanity\BackedEnumWithBoolType can have only "int" or "string" type.', + 71, + ], + [ + '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 c9a061443a..83d23411de 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -3,20 +3,30 @@ 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 \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInClassExtendsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInClassExtendsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInClassExtendsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -27,19 +37,20 @@ public function testRule(): void 'Class ExtendsImplements\Foo referenced with incorrect case: ExtendsImplements\FOO.', 15, ], + [ + 'Class ExtendsImplements\ExtendsFinalWithAnnotation extends @final class ExtendsImplements\FinalWithAnnotation.', + 43, + ], ]); } 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.', 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class ExtendsError\Lorem extends interface ExtendsError\BazInterface.', @@ -60,4 +71,91 @@ public function testRuleExtendsError(): void ]); } + public function testFinalByTag(): void + { + $this->analyse([__DIR__ . '/data/extends-final-by-tag.php'], [ + [ + 'Class ExtendsFinalByTag\Bar2 extends @final class ExtendsFinalByTag\Bar.', + 21, + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/class-extends-enum.php'], [ + [ + 'Class ClassExtendsEnum\Foo extends enum ClassExtendsEnum\FooEnum.', + 10, + ], + [ + 'Anonymous class extends enum ClassExtendsEnum\FooEnum.', + 16, + ], + ]); + } + + 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 424080859b..26cf177300 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php @@ -3,24 +3,41 @@ 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 \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInInstanceOfRuleTest extends \PHPStan\Testing\RuleTestCase +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( @@ -30,15 +47,16 @@ 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', ], [ 'Using self outside of class scope.', 9, ], [ - 'Class InstanceOfNamespace\Foo referenced with incorrect case: InstanceOfNamespace\FOO.', + 'Class InstanceOfNamespaceRule\Foo referenced with incorrect case: InstanceOfNamespaceRule\FOO.', 13, ], [ @@ -49,7 +67,7 @@ public function testClassDoesNotExist(): void 'Using self outside of class scope.', 17, ], - ] + ], ); } @@ -58,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 42f73d58ef..f6e5ac6b5e 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php @@ -3,20 +3,30 @@ 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 \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInTraitUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInTraitUseRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInTraitUseRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -32,14 +42,11 @@ 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.', 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], /*[ 'Trait TraitUseError\BarTrait uses class TraitUseError\Foo.', @@ -56,6 +63,7 @@ public function testTraitUseError(): void [ 'Anonymous class uses unknown trait TraitUseError\FooTrait.', 27, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Anonymous class uses interface TraitUseError\Baz.', @@ -64,4 +72,22 @@ public function testTraitUseError(): void ]); } + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/trait-use-enum.php'], [ + [ + 'Class TraitUseEnum\Foo uses enum TraitUseEnum\FooEnum.', + 13, + ], + [ + 'Anonymous class uses enum TraitUseEnum\FooEnum.', + 20, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index 3c6262a73b..d51a34a621 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php @@ -3,20 +3,30 @@ 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 \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInClassImplementsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInClassImplementsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInClassImplementsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -32,14 +42,11 @@ 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.', 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class ImplementsError\Lorem implements class ImplementsError\Foo.', @@ -56,4 +63,38 @@ public function testRuleImplementsError(): void ]); } + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/class-implements-enum.php'], [ + [ + 'Class ClassImplementsEnum\Foo implements enum ClassImplementsEnum\FooEnum.', + 10, + ], + [ + 'Anonymous class implements enum ClassImplementsEnum\FooEnum.', + 16, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..7ed5797895 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -0,0 +1,73 @@ + + */ +class ExistingClassesInEnumImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new ExistingClassesInEnumImplementsRule( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-implements.php'], [ + [ + 'Interface EnumImplements\FooInterface referenced with incorrect case: EnumImplements\FOOInterface.', + 30, + ], + [ + 'Enum EnumImplements\Foo3 implements class EnumImplements\FooClass.', + 35, + ], + [ + 'Enum EnumImplements\Foo4 implements trait EnumImplements\FooTrait.', + 40, + ], + [ + 'Enum EnumImplements\Foo5 implements enum EnumImplements\FooEnum.', + 45, + ], + [ + 'Enum EnumImplements\Foo6 implements unknown interface EnumImplements\NonexistentInterface.', + 50, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Enum EnumImplements\FooEnum referenced with incorrect case: EnumImplements\FOOEnum.', + 55, + ], + [ + 'Enum EnumImplements\Foo7 implements enum EnumImplements\FooEnum.', + 55, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index dca85d2a7b..c6a3db9ad4 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php @@ -3,20 +3,30 @@ 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 \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInInterfaceExtendsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + $reflectionProvider, + true, ); } @@ -32,14 +42,11 @@ 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.', 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Interface InterfaceExtendsError\Lorem extends class InterfaceExtendsError\BazClass.', @@ -52,4 +59,18 @@ public function testRuleExtendsError(): void ]); } + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/interface-extends-enum.php'], [ + [ + 'Interface InterfaceExtendsEnum\Foo extends enum InterfaceExtendsEnum\FooEnum.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/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 27cf1173bc..2ef0c3d184 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -2,21 +2,27 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleInstanceOfRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleInstanceOfRuleTest extends RuleTestCase { - /** @var bool */ - private $checkAlwaysTrueInstanceOf; + private bool $treatPhpDocTypesAsCertain; - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ImpossibleInstanceOfRule($this->checkAlwaysTrueInstanceOf, $this->treatPhpDocTypesAsCertain); + return new ImpossibleInstanceOfRule( + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -26,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( @@ -61,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, ], [ @@ -118,7 +123,6 @@ public function testInstanceof(): void [ 'Instanceof between *NEVER* and ImpossibleInstanceOf\Foo will always evaluate to false.', 234, - $tipText, ], [ 'Instanceof between ImpossibleInstanceOf\Bar&ImpossibleInstanceOf\Foo and ImpossibleInstanceOf\Foo will always evaluate to true.', @@ -146,130 +150,45 @@ public function testInstanceof(): void 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarGrandChild will always evaluate to false.', 322, ], - [ + /*[ 'Instanceof between mixed and int results in an error.', 353, ], [ 'Instanceof between mixed and ImpossibleInstanceOf\InvalidTypeTest|int results in an error.', 362, - ], + ],*/ [ 'Instanceof between ImpossibleInstanceOf\Foo and ImpossibleInstanceOf\Foo will always evaluate to true.', 388, $tipText, ], + [ + 'Instanceof between class-string and DateTimeInterface will always evaluate to false.', + 418, + $tipText, + ], [ '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, ], - ] - ); - } - - 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, - $tipText, - ], - [ - '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 class-string will always evaluate to false.', - 419, - $tipText, - ], - [ - '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'], [ [ @@ -281,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, ], ]); @@ -293,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'], [ [ @@ -325,4 +243,304 @@ public function testReportTypesFromPhpDocs(): void ]); } + public function testBug3096(): void + { + $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 new file mode 100644 index 0000000000..d30f3a29f6 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php @@ -0,0 +1,29 @@ + + */ +class InstantiationCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InstantiationCallableRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/instantiation-callable.php'], [ + [ + 'Cannot create callable from the new operator.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 0373e482ee..eb547315cf 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -3,30 +3,41 @@ namespace PHPStan\Rules\Classes; 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\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InstantiationRuleTest extends \PHPStan\Testing\RuleTestCase +class InstantiationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( - $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), true, true, true, true), - new ClassCaseSensitivityCheck($broker) + $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'], [ @@ -45,6 +56,7 @@ public function testInstantiation(): void [ 'Instantiated class TestInstantiation\FooBarInstantiation not found.', 27, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class TestInstantiation\BarInstantiation constructor invoked with 0 parameters, 1 required.', @@ -61,6 +73,7 @@ public function testInstantiation(): void [ 'Instantiated class Test not found.', 33, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class DatePeriod constructor invoked with 0 parameters, 1-4 required.', @@ -78,10 +91,6 @@ public function testInstantiation(): void 'Using parent outside of class scope.', 41, ], - [ - 'Class TestInstantiation\BarInstantiation constructor invoked with 0 parameters, 1 required.', - 44, - ], [ 'Class TestInstantiation\InstantiatingClass constructor invoked with 0 parameters, 1 required.', 57, @@ -153,14 +162,17 @@ public function testInstantiation(): void [ 'Instantiated class UndefinedClass1 not found.', 169, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Instantiated class UndefinedClass2 not found.', 172, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Instantiated class UndefinedClass3 not found.', 179, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Class TestInstantiation\FinalClass does not have a constructor and must be instantiated without any parameters.', @@ -186,7 +198,7 @@ public function testInstantiation(): void 'Class TestInstantiation\ClassExtendingAbstractConstructor constructor invoked with 0 parameters, 1 required.', 273, ], - ] + ], ); } @@ -196,10 +208,10 @@ public function testSoap(): void [__DIR__ . '/data/instantiation-soap.php'], [ [ - 'Parameter #2 $faultstring of class SoapFault constructor expects string, int given.', + 'Parameter #2 $string of class SoapFault constructor expects string, int given.', 6, ], - ] + ], ); } @@ -218,4 +230,330 @@ public function testBug3404(): void ]); } + public function testOldStyleConstructorOnPhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/php80-constructor.php'], [ + [ + 'Class OldStyleConstructorOnPhp8 does not have a constructor and must be instantiated without any parameters.', + 13, + ], + [ + 'Class OldStyleConstructorOnPhp8 does not have a constructor and must be instantiated without any parameters.', + 20, + ], + ]); + } + + public function testOldStyleConstructorOnPhp7(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.x'); + } + + $this->analyse([__DIR__ . '/data/php80-constructor.php'], [ + [ + 'Class OldStyleConstructorOnPhp8 constructor invoked with 0 parameters, 1 required.', + 19, + ], + ]); + } + + public function testBug4030(): void + { + $this->analyse([__DIR__ . '/data/bug-4030.php'], []); + } + + public function testPromotedProperties(): void + { + $this->analyse([__DIR__ . '/data/instantiation-promoted-properties.php'], [ + [ + 'Parameter #2 $bar of class InstantiationPromotedProperties\Foo constructor expects array, array given.', + 30, + ], + [ + '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, + ], + ]); + } + + public function testBug4056(): void + { + $this->analyse([__DIR__ . '/data/bug-4056.php'], []); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/instantiation-named-arguments.php'], [ + [ + 'Missing parameter $j (int) in call to InstantiationNamedArguments\Foo constructor.', + 15, + ], + [ + 'Unknown parameter $z in call to InstantiationNamedArguments\Foo constructor.', + 16, + ], + ]); + } + + public function testBug4471(): void + { + $this->analyse([__DIR__ . '/data/bug-4471.php'], [ + [ + 'Instantiated class Bug4471\Baz not found.', + 19, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Instantiated class Bug4471\Foo is abstract.', + 24, + ], + [ + 'Cannot instantiate interface Bug4471\Bar.', + 27, + ], + ]); + } + + public function testBug1711(): void + { + $this->analyse([__DIR__ . '/data/bug-1711.php'], []); + } + + public function testBug3425(): void + { + $this->analyse([__DIR__ . '/data/bug-3425.php'], [ + [ + 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects T of IteratorAggregate|RecursiveIterator, Generator given.', + 5, + ], + ]); + } + + public function testBug5002(): void + { + $this->analyse([__DIR__ . '/data/bug-5002.php'], []); + } + + public function testBug4681(): void + { + $this->analyse([__DIR__ . '/data/bug-4681.php'], []); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1 and static reflection.'); + } + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-instantiation-callable.php'], []); + } + + public function testEnumInstantiation(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-instantiation.php'], [ + [ + 'Cannot instantiate enum EnumInstantiation\Foo.', + 9, + ], + [ + 'Cannot instantiate enum EnumInstantiation\Foo.', + 14, + ], + [ + 'Cannot instantiate enum EnumInstantiation\Foo.', + 21, + ], + ]); + } + + public function testBug6370(): void + { + $this->analyse([__DIR__ . '/data/bug-6370.php'], [ + [ + 'Parameter #1 $something of class Bug6370\A constructor expects string, int given.', + 45, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..2ce3e7e268 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -0,0 +1,122 @@ + + */ +class InvalidPromotedPropertiesRuleTest extends RuleTestCase +{ + + private int $phpVersion; + + protected function getRule(): Rule + { + return new InvalidPromotedPropertiesRule(new PhpVersion($this->phpVersion)); + } + + public function testNotSupportedOnPhp7(): void + { + $this->phpVersion = 70400; + $this->analyse([__DIR__ . '/data/invalid-promoted-properties.php'], [ + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 8, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 10, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 17, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 21, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 23, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 31, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 38, + ], + [ + 'Promoted properties are supported only on PHP 8.0 and later.', + 45, + ], + ]); + } + + public function testSupportedOnPhp8(): void + { + $this->phpVersion = 80000; + $this->analyse([__DIR__ . '/data/invalid-promoted-properties.php'], [ + [ + 'Promoted properties can be in constructor only.', + 10, + ], + [ + 'Promoted properties can be in constructor only.', + 17, + ], + [ + 'Promoted properties can be in constructor only.', + 21, + ], + [ + 'Promoted properties can be in constructor only.', + 23, + ], + [ + 'Promoted properties are not allowed in abstract constructors.', + 31, + ], + [ + 'Promoted properties are not allowed in abstract constructors.', + 38, + ], + [ + 'Promoted property parameter $i can not be variadic.', + 45, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..12631f5c74 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -0,0 +1,165 @@ + + */ +class LocalTypeAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new LocalTypeAliasesRule( + 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 + { + $this->analyse([__DIR__ . '/data/local-type-aliases.php'], [ + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\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 LocalTypeAliases\int does not exist.', + 39, + ], + [ + 'Cannot import type alias ImportedAliasFromUnknownClass: class LocalTypeAliases\UnknownClass does not exist.', + 39, + ], + [ + 'Cannot import type alias ImportedUnknownAlias: type alias does not exist in LocalTypeAliases\Foo.', + 39, + ], + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeAliases\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, + ], + [ + '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, + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/local-type-aliases-enums.php'], [ + [ + 'Cannot import type alias Test: class LocalTypeAliasesEnums\NonexistentClass does not exist.', + 8, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/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 5d7b7c061d..d7c804c237 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -3,11 +3,14 @@ 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; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -20,12 +23,21 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new MixinRule( - self::getContainer()->getByType(FileTypeMapper::class), - $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider), - new GenericObjectTypeCheck(), - new MissingTypehintCheck($reflectionProvider, true, true), - 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, + ), ); } @@ -45,7 +57,7 @@ public function testRule(): void 34, ], [ - 'Generic type Traversable in PHPDoc tag @mixin specifies 3 template types, but class Traversable supports only 2: TKey, TValue', + 'Generic type Traversable in PHPDoc tag @mixin specifies 3 template types, but interface Traversable supports only 2: TKey, TValue', 34, ], [ @@ -55,11 +67,11 @@ 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.', 50, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'PHPDoc tag @mixin contains invalid type MixinRule\FooTrait.', @@ -68,6 +80,7 @@ public function testRule(): void [ 'PHPDoc tag @mixin contains unknown class MixinRule\U.', 59, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Generic type MixinRule\Consecteur in PHPDoc tag @mixin does not specify all template types of class MixinRule\Consecteur: T, U', @@ -77,6 +90,42 @@ public function testRule(): void 'Class MixinRule\Foo referenced with incorrect case: MixinRule\foo.', 84, ], + [ + '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, + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/mixin-enums.php'], [ + [ + 'PHPDoc tag @mixin contains non-object type int.', + 16, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/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 8f35d2c383..23501ec856 100644 --- a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NewStaticRuleTest extends \PHPStan\Testing\RuleTestCase +class NewStaticRuleTest extends RuleTestCase { protected function getRule(): Rule @@ -18,7 +19,7 @@ protected function getRule(): Rule public function testRule(): void { $error = 'Unsafe usage of new static().'; - $tipText = 'Consider making the class or the constructor final.'; + $tipText = 'See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static'; $this->analyse([__DIR__ . '/data/new-static.php'], [ [ $error, @@ -33,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 new file mode 100644 index 0000000000..f3e2b90cae --- /dev/null +++ b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php @@ -0,0 +1,56 @@ + + */ +class NonClassAttributeClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NonClassAttributeClassRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/non-class-attribute-class.php'], [ + [ + 'Interface cannot be an Attribute class.', + 5, + ], + /* [ reported by a separate rule + 'Trait cannot be an Attribute class.', + 11, + ], */ + [ + 'Abstract class NonClassAttributeClass\Lorem cannot be an Attribute class.', + 23, + ], + [ + 'Attribute class NonClassAttributeClass\Ipsum constructor must be public.', + 29, + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/enum-cannot-be-attribute.php'], [ + [ + 'Enum cannot be an Attribute class.', + 5, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/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..03f3361bef --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php @@ -0,0 +1,96 @@ + + */ +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, + ], + [ + 'Interface IncompatibleRequireExtends\RequireNonExisstentUnionClassinterface requires implementing class to extend IncompatibleRequireExtends\NonExistentClass|IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\RequireNonExisstentUnionClassinterface@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:185 does not.', + 185, + ], + [ + 'Interface IncompatibleRequireExtends\RequireNonExisstentUnionClassinterface requires implementing class to extend IncompatibleRequireExtends\NonExistentClass|IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\SomeClass@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:187 does not.', + 187, + ], + [ + 'Trait IncompatibleRequireExtends\RequireNonExisstentUnionClassTrait requires using class to extend IncompatibleRequireExtends\NonExistentClass|IncompatibleRequireExtends\SomeClass, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:194 does not.', + 194, + ], + [ + 'Trait IncompatibleRequireExtends\RequireNonExisstentUnionClassTrait requires using class to extend IncompatibleRequireExtends\NonExistentClass|IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\SomeClass@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:198 does not.', + 198, + ], + ]; + + $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 new file mode 100644 index 0000000000..e22377bd1c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php @@ -0,0 +1,29 @@ + + */ +class TraitAttributeClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new TraitAttributeClassRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/non-class-attribute-class.php'], [ + [ + 'Trait cannot be an Attribute class.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index bb15fd5700..542ba55e5e 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -2,21 +2,29 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class UnusedConstructorParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class UnusedConstructorParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $reportExactLine = true; + + protected function getRule(): Rule { - return new UnusedConstructorParametersRule(new UnusedFunctionParametersCheck()); + 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.', @@ -29,4 +37,43 @@ public function testUnusedConstructorParameters(): void ]); } + public function testUnusedConstructorParameters(): void + { + $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'], []); + } + + 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/access-private-constant-static.php b/tests/PHPStan/Rules/Classes/data/access-private-constant-static.php new file mode 100644 index 0000000000..84fbffba7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/access-private-constant-static.php @@ -0,0 +1,29 @@ +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-1711.php b/tests/PHPStan/Rules/Classes/data/bug-1711.php new file mode 100644 index 0000000000..f41785b8a2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-1711.php @@ -0,0 +1,38 @@ +x = $x; + } + + public function setX(?float $x) { + $this->x = $x; + } +} + +class CorrectBehavior { + private $x; + + public function __construct(float $x) { + $this->x = $x; + } + + public function setX(float $x) { + $this->x = $x; + } +} + +function (): void { + $items = [0.5, 1]; + + foreach ($items as $item) { + $wrong = new WrongBehavior($item); + $wrong->setX($item); + $correct = new CorrectBehavior($item); + $correct->setX($item); + } +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-1917.php b/tests/PHPStan/Rules/Classes/data/bug-1917.php new file mode 100644 index 0000000000..d9789f728c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-1917.php @@ -0,0 +1,22 @@ +a = $a; + $this->b = $b; + + var_dump([$this->a, $this->b]); + } +} + +class B extends A { + function __construct($a, $b) { + parent::__construct(...func_get_args()); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-3096.php b/tests/PHPStan/Rules/Classes/data/bug-3096.php new file mode 100644 index 0000000000..42c332b5da --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3096.php @@ -0,0 +1,14 @@ + $class + */ + public static function sayHello(\DateTimeInterface $object, string $class): void + { + assert($object instanceof $class); + } +} 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-3425.php b/tests/PHPStan/Rules/Classes/data/bug-3425.php new file mode 100644 index 0000000000..534b5c715f --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3425.php @@ -0,0 +1,5 @@ + new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + ], + ]), + 'Dummy-list' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + ], + ]), + 'Dummy-list_details' => new \ArrayObject([ + 'properties' => [ + 'id' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'description' => [ + 'type' => 'string', + ], + 'relatedDummy' => new \ArrayObject([ + '$ref' => '#/definitions/RelatedDummy-list_details', + ]), + ], + ]), + 'Dummy:OutputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'baz' => new \ArrayObject([ + 'readOnly' => true, + 'type' => 'string', + ]), + 'bat' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'Dummy:InputDto' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'foo' => new \ArrayObject([ + 'type' => 'string', + ]), + 'bar' => new \ArrayObject([ + 'type' => 'integer', + ]), + ], + ]), + 'RelatedDummy-list_details' => new \ArrayObject([ + 'type' => 'object', + 'properties' => [ + 'name' => new \ArrayObject([ + 'type' => 'string', + ]), + ], + ]), +]); diff --git a/tests/PHPStan/Rules/Classes/data/bug-4471.php b/tests/PHPStan/Rules/Classes/data/bug-4471.php new file mode 100644 index 0000000000..46818e2ad7 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-4471.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug4681; + +class A { + /** @param array{a: int} $row */ + public function __construct( + public array $row + ) {} +} + +function (): void { + /** @var list */ + $rows = []; + + $entries = array_map( + static fn (array $row) : A => new A($row), + $rows + ); +}; + +function (): void { + /** @var list */ + $rows = []; + + $entries = array_map( + /** @param array{a: int} $row */ + static fn (array $row) : A => new A($row), + $rows + ); +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-4689.php b/tests/PHPStan/Rules/Classes/data/bug-4689.php new file mode 100644 index 0000000000..e1df395690 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-4689.php @@ -0,0 +1,34 @@ +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-5002.php b/tests/PHPStan/Rules/Classes/data/bug-5002.php new file mode 100644 index 0000000000..605e4c5c04 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-5002.php @@ -0,0 +1,18 @@ +', $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-6370.php b/tests/PHPStan/Rules/Classes/data/bug-6370.php new file mode 100644 index 0000000000..5744316ed3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-6370.php @@ -0,0 +1,53 @@ += 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 new file mode 100644 index 0000000000..c0cf4238fe --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-attributes.php @@ -0,0 +1,168 @@ += 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-attribute.php b/tests/PHPStan/Rules/Classes/data/class-constant-attribute.php new file mode 100644 index 0000000000..032fe761ba --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-constant-attribute.php @@ -0,0 +1,30 @@ += 8.0 + +namespace ClassConstantNullsafeNamespace; + +class Foo { + public const LOREM = 'lorem'; + +} +class Bar +{ + public Foo $foo; +} + +function doFoo(?Bar $bar) +{ + $bar?->foo::LOREM; +} diff --git a/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php new file mode 100644 index 0000000000..137b2f58b7 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php @@ -0,0 +1,22 @@ += 8.0 + +namespace ClassConstantOnExpr; + +class Foo +{ + + public function doFoo( + \stdClass $std, + string $string, + ?\stdClass $stdOrNull, + ?string $stringOrNull + ): void + { + echo $std::class; + echo $string::class; + echo $stdOrNull::class; + echo $stringOrNull::class; + echo 'Foo'::class; + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php b/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php index dbb4a23e96..f4be1b9a9f 100644 --- a/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php +++ b/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php @@ -1,4 +1,4 @@ -= 8.1 + +namespace ClassExtendsEnum; + +enum FooEnum +{ + +} + +class Foo extends FooEnum +{ + +} + +function (): void { + new class() extends FooEnum { + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/class-implements-enum.php b/tests/PHPStan/Rules/Classes/data/class-implements-enum.php new file mode 100644 index 0000000000..a6bcab0e7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-implements-enum.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ClassImplementsEnum; + +enum FooEnum +{ + +} + +class Foo implements FooEnum +{ + +} + +function (): void { + new class() implements FooEnum { + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/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.1 + +namespace DuplicatedEnumCase; + +enum Foo +{ + case BAR; + case FOO; + case bar; + case BAR; +} + +enum Boo +{ + const BAR = 0; + const bar = 0; + case BAR; +} + +enum Hoo +{ + case BAR; + const BAR = 0; +} diff --git a/tests/PHPStan/Rules/Classes/data/duplicate-promoted-property.php b/tests/PHPStan/Rules/Classes/data/duplicate-promoted-property.php new file mode 100644 index 0000000000..a5d7cd93e9 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/duplicate-promoted-property.php @@ -0,0 +1,19 @@ += 8.0 + +namespace DuplicatedPromotedProperty; + +class Foo +{ + + private $foo; + + public function __construct( + private $foo, + private $bar, + private $bar + ) + { + + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php new file mode 100644 index 0000000000..10809e566a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php @@ -0,0 +1,48 @@ += 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-attributes.php b/tests/PHPStan/Rules/Classes/data/enum-attributes.php new file mode 100644 index 0000000000..d6faaf1d12 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-attributes.php @@ -0,0 +1,39 @@ += 8.1 + +namespace EnumAttributes; + +#[\Attribute] +class AttributeWithoutSpecificTarget +{ + +} + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AttributeWithPropertyTarget +{ + +} + +#[AttributeWithoutSpecificTarget] +enum EnumWithValidClassAttribute +{ + +} + +#[AttributeWithPropertyTarget] +enum EnumWithInvalidClassAttribute +{ + +} + +#[\Attribute] +enum EnumAsAttribute +{ + +} + +#[EnumAsAttribute] +class ClassWithEnumAttribute +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php b/tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php new file mode 100644 index 0000000000..21a0f7230d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php @@ -0,0 +1,9 @@ += 8.1 + +namespace EnumAsAttribute; + +#[\Attribute] +enum Foo +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-implements.php b/tests/PHPStan/Rules/Classes/data/enum-implements.php new file mode 100644 index 0000000000..6ce3e6e85d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-implements.php @@ -0,0 +1,58 @@ += 8.1 + +namespace EnumImplements; + +interface FooInterface +{ + +} + +class FooClass +{ + +} + +trait FooTrait +{ + +} + +enum FooEnum +{ + +} + +enum Foo implements FooInterface +{ + +} + +enum Foo2 implements FOOInterface +{ + +} + +enum Foo3 implements FooClass +{ + +} + +enum Foo4 implements FooTrait +{ + +} + +enum Foo5 implements FooEnum +{ + +} + +enum Foo6 implements NonexistentInterface +{ + +} + +enum Foo7 implements FOOEnum +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-instantiation.php b/tests/PHPStan/Rules/Classes/data/enum-instantiation.php new file mode 100644 index 0000000000..708c7b2a31 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-instantiation.php @@ -0,0 +1,23 @@ += 8.1 + +namespace EnumInstantiation; + +enum Foo +{ + public function createSelf() + { + return new self(); + } + + public function createStatic() + { + return new static(); + } +} + +class Boo +{ + public static function createFoo() { + return new Foo(); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-sanity.php b/tests/PHPStan/Rules/Classes/data/enum-sanity.php new file mode 100644 index 0000000000..1698b595fd --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-sanity.php @@ -0,0 +1,123 @@ += 8.1 + +namespace EnumSanity; + +enum EnumWithAbstractMethod +{ + abstract function foo(); +} + +enum EnumWithConstructorAndDestructor +{ + public function __construct() + {} + + public function __destruct() + {} +} + +enum EnumWithMagicMethods +{ + public function __get() + {} + + public function __call() + {} + + public function __callStatic() + {} + + public function __set() + {} + + public function __invoke() + {} +} + +enum PureEnumCannotRedeclareMethods +{ + public static function cases() + { + } + + public static function tryFrom() + { + } + + public static function from() + { + } +} + +enum BackedEnumCannotRedeclareMethods: int +{ + public static function cases() + { + } + + public static function tryFrom() + { + } + + public static function from() + { + } +} + +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-final-by-tag.php b/tests/PHPStan/Rules/Classes/data/extends-final-by-tag.php new file mode 100644 index 0000000000..501c6e8f00 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-final-by-tag.php @@ -0,0 +1,24 @@ + '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 @@ +branch1 instanceof \SimpleXMLElement; + echo $xml->branch2->branch3 instanceof \SimpleXMLElement; + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/instanceof-defined.php b/tests/PHPStan/Rules/Classes/data/instanceof-defined.php index 1b26b39c71..ea6a11a9cc 100644 --- a/tests/PHPStan/Rules/Classes/data/instanceof-defined.php +++ b/tests/PHPStan/Rules/Classes/data/instanceof-defined.php @@ -1,6 +1,6 @@ = 8.0 + +namespace InstantiationNamedArguments; + +class Foo +{ + + public function __construct(int $i, int $j) + { + + } + + public function doFoo() + { + $s = new self(i: 1); + $r = new self(i: 1, j: 2, z: 3); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php b/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php new file mode 100644 index 0000000000..d113ac2f70 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php @@ -0,0 +1,47 @@ + */private array $bar + ) { } + +} + +class Bar +{ + + /** + * @param array $bar + */ + public function __construct( + private array $foo, + private array $bar + ) { } + +} + +function () { + new Foo([], ['foo']); + new Foo([], [1]); + + new Bar([], ['foo']); + new Bar([], [1]); +}; + +class PromotedPropertyNotNullable +{ + + public function __construct( + private int $intProp = null, + ) {} + +} + +function () { + new PromotedPropertyNotNullable(null); +}; diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-soap.php b/tests/PHPStan/Rules/Classes/data/instantiation-soap.php index 2025fd82ea..1ba5a85d98 100644 --- a/tests/PHPStan/Rules/Classes/data/instantiation-soap.php +++ b/tests/PHPStan/Rules/Classes/data/instantiation-soap.php @@ -4,5 +4,21 @@ function () { throw new \SoapFault('Server', 123); +}; + +function () { throw new \SoapFault('Server', 'Some error message'); }; + +function () { + throw new \SoapFault('Server', 'Some error message', 'actor', [], 'name', []); +}; + +function () { + throw new \SoapFault('Server', 'Some error message', 'actor', 'test', 'name', 'test'); +}; + + +function () { + throw new \SoapFault('Server', 'Some error message', 'actor', 1, 'name', 2); +}; diff --git a/tests/PHPStan/Rules/Classes/data/instantiation.php b/tests/PHPStan/Rules/Classes/data/instantiation.php index dfce778521..58c5b2cf60 100644 --- a/tests/PHPStan/Rules/Classes/data/instantiation.php +++ b/tests/PHPStan/Rules/Classes/data/instantiation.php @@ -1,4 +1,4 @@ -= 8.1 + +namespace InterfaceExtendsEnum; + +enum FooEnum +{ + +} + +interface Foo extends FooEnum +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php b/tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php new file mode 100644 index 0000000000..e0933face1 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php @@ -0,0 +1,21 @@ + 'foo'; + +function foo(public $i): void +{ + +} + +class Bar +{ + + abstract public function __construct(public $i); + +} + +interface Baz +{ + + function __construct(public $i); + +} + +class Lorem +{ + + public function __construct(public ...$i) {} + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases-enums.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases-enums.php new file mode 100644 index 0000000000..f2e0c58915 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases-enums.php @@ -0,0 +1,11 @@ += 8.1 + +namespace LocalTypeAliasesEnums; + +/** + * @phpstan-import-type Test from NonexistentClass + */ +enum Foo +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php new file mode 100644 index 0000000000..152e77d8d7 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php @@ -0,0 +1,106 @@ + + */ +class 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 + */ +class 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 + */ +class Baz +{ +} + +/** + * @phpstan-import-type CircularTypeAliasImport2 from Baz + * @phpstan-type CircularTypeAliasImport1 CircularTypeAliasImport2 + */ +class Qux +{ +} + +/** + * @phpstan-template T + * @phpstan-type T never + */ +class Generic +{ +} + +/** + * @phpstan-type InvalidTypeAlias invalid-type-definition + */ +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-enums.php b/tests/PHPStan/Rules/Classes/data/mixin-enums.php new file mode 100644 index 0000000000..cc35ec2b48 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/mixin-enums.php @@ -0,0 +1,24 @@ += 8.1 + +namespace MixinEnums; + +/** + * @mixin \Exception + */ +enum Foo +{ + +} + +/** + * @mixin int + */ +enum Bar +{ + +} + +enum Baz +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin-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 63fa7cb7e4..b6ab1b092b 100644 --- a/tests/PHPStan/Rules/Classes/data/mixin.php +++ b/tests/PHPStan/Rules/Classes/data/mixin.php @@ -85,3 +85,51 @@ class Amet { } + +/** + * @mixin int + */ +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/Classes/data/trait-use-enum.php b/tests/PHPStan/Rules/Classes/data/trait-use-enum.php new file mode 100644 index 0000000000..6e741d3909 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/trait-use-enum.php @@ -0,0 +1,23 @@ += 8.1 + +namespace TraitUseEnum; + +enum FooEnum +{ + +} + +class Foo +{ + + use FooEnum; + +} + +function (): void { + new class() { + + use FooEnum; + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-promoted-properties.php b/tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-promoted-properties.php new file mode 100644 index 0000000000..7823cbec22 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-promoted-properties.php @@ -0,0 +1,18 @@ += 8.0 + +namespace UnusedConstructorParametersPromotedProperties; + +class Foo +{ + + private int $y; + + public function __construct( + public int $x, + int $y + ) + { + $this->y = $y; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index a6cc1f0256..3115a897b7 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -2,16 +2,20 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class BooleanAndConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class BooleanAndConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new BooleanAndConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +23,13 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -54,7 +60,7 @@ public function testRule(): void 27, ], [ - 'Right side of && is always false.', + 'Result of && is always false.', 30, ], [ @@ -96,6 +102,115 @@ public function testRule(): void 'Result of && is always false.', 125, ], + [ + 'Left side of && is always false.', + 139, + ], + [ + 'Right side of && is always false.', + 141, + ], + [ + 'Left side of && is always true.', + 145, + ], + [ + 'Right side of && is always true.', + 147, + ], + [ + '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, + ], ]); } @@ -159,7 +274,6 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array /** * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain */ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { @@ -167,4 +281,162 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs $this->analyse([__DIR__ . '/data/boolean-and-treat-phpdoc-types-regression.php'], []); } + public function testBugComposerDependentVariables(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-composer-dependent-variables.php'], []); + } + + public function testBug2231(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-2231.php'], [ + [ + 'Result of && is always false.', + 21, + ], + ]); + } + + public function testBug1746(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-1746.php'], [ + [ + 'Left side of && is always true.', + 20, + ], + ]); + } + + public function testBug4666(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4666.php'], []); + } + + public function testBug2870(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2870.php'], []); + } + + 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 76f3a90f95..f9bef9b5d1 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -2,16 +2,20 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class BooleanNotConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class BooleanNotConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new BooleanNotConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +23,13 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -52,6 +58,19 @@ public function testRule(): void 'Negated boolean expression is always false.', 40, ], + [ + 'Negated boolean expression is always true.', + 46, + ], + [ + '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.', + ], ]); } @@ -96,7 +115,6 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array /** * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain */ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { @@ -104,4 +122,82 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs $this->analyse([__DIR__ . '/../DeadCode/data/bug-without-issue-1.php'], []); } + public function testBug6473(): void + { + $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 b4aa176731..ca46233349 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -2,16 +2,21 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class BooleanOrConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class BooleanOrConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new BooleanOrConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +24,13 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -58,7 +65,7 @@ public function testRule(): void 30, ], [ - 'Right side of || is always false.', + 'Result of || is always true.', 33, ], [ @@ -87,6 +94,106 @@ public function testRule(): void 'Result of || is always true.', 65, ], + [ + 'Left side of || is always false.', + 77, + ], + [ + 'Right side of || is always false.', + 79, + ], + [ + 'Left side of || is always true.', + 83, + ], + [ + 'Right side of || is always true.', + 85, + ], + [ + '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, + ], ]); } @@ -150,7 +257,6 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array /** * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain */ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { @@ -158,4 +264,115 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs $this->analyse([__DIR__ . '/data/boolean-or-treat-phpdoc-types-regression.php'], []); } + public function testBug6258(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6258.php'], []); + } + + 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 new file mode 100644 index 0000000000..38f3237a45 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -0,0 +1,68 @@ + + */ +class DoWhileLoopConstantConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new DoWhileLoopConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + true, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/do-while-loop.php'], [ + [ + 'Do-while loop condition is always true.', + 12, + ], + [ + 'Do-while loop condition is always false.', + 37, + ], + [ + 'Do-while loop condition is always false.', + 46, + ], + [ + 'Do-while loop condition is always false.', + 55, + ], + [ + 'Do-while loop condition is always true.', + 64, + ], + [ + 'Do-while loop condition is always false.', + 73, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 31cb731e65..f337950a0f 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -2,16 +2,21 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ElseIfConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class ElseIfConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ElseIfConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +24,13 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -32,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 @@ -50,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.', ], ]); } @@ -61,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 fcc1be770c..25e362f6cc 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -2,23 +2,19 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Expr\FuncCall; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\Type; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IfConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class IfConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new IfConstantConditionRule( new ConstantConditionRuleHelper( @@ -26,11 +22,12 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + true, ); } @@ -39,28 +36,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - /** - * @return DynamicFunctionReturnTypeExtension[] - */ - public function getDynamicFunctionReturnTypeExtensions(): array - { - return [ - new class implements DynamicFunctionReturnTypeExtension { - - public function isFunctionSupported(FunctionReflection $functionReflection): bool - { - return $functionReflection->getName() === 'always_true'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - return new ConstantBooleanType(true); - } - - }, - ]; - } - public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -77,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.', @@ -90,6 +66,14 @@ public function testRule(): void 'If condition is always true.', 127, ], + [ + 'If condition is always true.', + 287, + ], + [ + 'If condition is always false.', + 291, + ], ]); } @@ -120,4 +104,85 @@ public function testReportPhpDoc(): void ]); } + public function testBug4043(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4043.php'], [ + [ + 'If condition is always false.', + 43, + ], + [ + 'If condition is always true.', + 50, + ], + ]); + } + + 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 aca83e1d5b..219e0450c1 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -2,29 +2,37 @@ namespace PHPStan\Rules\Comparison; +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; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeFunctionCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { - /** @var bool */ - private $checkAlwaysTrueCheckTypeFunctionCall; + private bool $treatPhpDocTypesAsCertain; - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ImpossibleCheckTypeFunctionCallRule( new ImpossibleCheckTypeHelper( $this->createReflectionProvider(), $this->getTypeSpecifier(), - [\stdClass::class], - $this->treatPhpDocTypesAsCertain + [stdClass::class], + $this->treatPhpDocTypesAsCertain, ), - $this->checkAlwaysTrueCheckTypeFunctionCall, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ); } @@ -35,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'], @@ -81,262 +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, - ], - [ - 'Call to function in_array() with arguments int, array(\'foo\', \'bar\') and true will always evaluate to false.', - 235, + 220, ], [ - '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 int, array{\'foo\', \'bar\'} and true will always evaluate to false.', + 246, ], [ - 'Call to function in_array() with arguments \'bar\'|\'foo\', array(\'foo\', \'bar\') and true will always evaluate to true.', - 248, + 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', + 255, ], [ - 'Call to function in_array() with arguments \'foo\', array(\'foo\') and true will always evaluate to true.', - 252, + 'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', + 263, ], [ - 'Call to function in_array() with arguments \'foo\', array(\'foo\', \'bar\') and true will always evaluate to true.', - 256, + 'Call to function in_array() with arguments \'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', + 267, ], [ - '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 \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', + 331, ], [ - '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 \'baz\', array{0: \'bar\', 1?: \'foo\'} and true will always evaluate to false.', + 347, ], [ - 'Call to function in_array() with arguments \'foo\', array() and true will always evaluate to false.', - 343, + 'Call to function in_array() with arguments \'foo\', array{} and true will always evaluate to false.', + 354, ], [ - 'Call to function array_key_exists() with \'a\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to true.', - 360, + 'Call to function array_key_exists() with \'a\' and array{a: 1, b?: 2} will always evaluate to true.', + 371, ], [ - 'Call to function array_key_exists() with \'c\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to false.', - 366, + 'Call to function array_key_exists() with \'c\' and array{a: 1, b?: 2} will always evaluate to false.', + 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.', - 609, + 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.', + 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 is_numeric() with \'123\' will always evaluate to true.', - 692, - ], - [ - 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', - 693, - ], [ 'Call to function assert() with true will always evaluate to true.', - 700, - ], - [ - 'Call to function is_numeric() with 123|float will always evaluate to true.', - 700, - ], - [ - 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', - 782, - ], - [ - 'Call to function assert() with bool will always evaluate to true.', - 786, - ], - [ - '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, + 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 assert() with false will always evaluate to false.', - 48, + 'Call to function is_numeric() with \'123\' will always evaluate to true.', + 718, ], [ - 'Call to function is_callable() with \'nonexistentFunction\' will always evaluate to false.', - 87, + 'Call to function assert() with false will always evaluate to false.', + 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.', - 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, + 719, ], [ - 'Call to function is_callable() with mixed will always evaluate to false.', - 571, + 'Call to function assert() with true will always evaluate to true.', + 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 method_exists() with \'UndefinedClass\' and string will always evaluate to false.', - 594, + 'Call to function is_numeric() with 123|float will always evaluate to true.', + 726, ], [ - 'Call to function method_exists() with \'UndefinedClass\' and \'test\' will always evaluate to false.', - 597, + 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', + 809, ], [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 630, + 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', + 813, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', - 639, + '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.', - 648, + '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'], [ [ @@ -348,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'], [ [ @@ -360,6 +311,696 @@ 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->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2550.php'], []); + } + + public function testBug3994(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3994.php'], []); + } + + public function testBug1613(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-1613.php'], []); + } + + public function testBug2714(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2714.php'], []); + } + + public function testBug4657(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-4657.php'], []); + } + + public function testBug4999(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-4999.php'], []); + } + + public function testArrayIsList(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/array-is-list.php'], [ + [ + 'Call to function array_is_list() with array will always evaluate to false.', + 13, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Call to function array_is_list() with array{foo: \'bar\', bar: \'baz\'} will always evaluate to false.', + 40, + ], + [ + 'Call to function array_is_list() with array{0: \'foo\', foo: \'bar\', bar: \'baz\'} will always evaluate to false.', + 44, + ], + ]); + } + + public function testBug3766(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3766.php'], []); + } + + public function testBug6305(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6305.php'], [ + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.', + 11, + ], + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\B\' will always evaluate to false.', + 14, + ], + ]); + } + + public function testBug6698(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6698.php'], []); + } + + public function testBug5369(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5369.php'], []); + } + + public function testBugInArrayDateFormat(): void + { + $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->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5496.php'], []); + } + + public function testBug3892(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3892.php'], []); + } + + public function testBug3314(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3314.php'], []); + } + + public function testBug2870(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2870.php'], []); + } + + public function testBug5354(): void + { + $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 new file mode 100644 index 0000000000..67245ebaba --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ImpossibleCheckTypeGenericOverwriteRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + true, + ), + true, + false, + true, + ); + } + + public function testNoReportedErrorOnOverwrite(): void + { + $this->analyse([__DIR__ . '/data/generic-type-override.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/impossible-check-type-generic-overwrite.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php new file mode 100644 index 0000000000..b81646c023 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -0,0 +1,139 @@ + + */ +class ImpossibleCheckTypeMethodCallRuleEqualsTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + true, + ), + true, + false, + true, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/impossible-method-call.php'], [ + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 14, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with int will always evaluate to false.', + 15, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with int will always evaluate to false.', + 30, + '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.', + 60, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 2 will always evaluate to false.', + 63, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and 1 will always evaluate to false.', + 66, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and 2 will always evaluate to true.', + 69, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with stdClass and stdClass will always evaluate to true.', + 78, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with stdClass and stdClass will always evaluate to false.', + 81, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', + 101, + '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.', + 113, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{} and array{} will always evaluate to false.', + 116, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{1, 3} and array{1, 3} will always evaluate to true.', + 119, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{1, 3} and array{1, 3} will always evaluate to false.', + 122, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 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, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'\' and \'\' will always evaluate to false.', + 175, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', + 191, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', + 194, + ], + [ + '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.', + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/impossible-check-type-method-call-equals.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 633a48b728..6bc2d8e2d5 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -2,36 +2,32 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Expr\MethodCall; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierAwareExtension; -use PHPStan\Analyser\TypeSpecifierContext; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension; -use PHPStan\Type\MethodTypeSpecifyingExtension; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - public function getRule(): \PHPStan\Rules\Rule + private bool $reportAlwaysTrueInLastCondition = false; + + public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( new ImpossibleCheckTypeHelper( $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, true, - $this->treatPhpDocTypesAsCertain ); } @@ -40,152 +36,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - /** - * @return MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new AssertionClassMethodTypeSpecifyingExtension(null), - new class() implements MethodTypeSpecifyingExtension, - TypeSpecifierAwareExtension { - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return \PHPStan\Tests\AssertionClass::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context - ): bool - { - return $methodReflection->getName() === 'assertNotInt' - && count($node->args) > 0; - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context - ): SpecifiedTypes - { - return $this->typeSpecifier->specifyTypesInCondition( - $scope, - new \PhpParser\Node\Expr\BooleanNot( - new \PhpParser\Node\Expr\FuncCall( - new \PhpParser\Node\Name('is_int'), - [ - $node->args[0], - ] - ) - ), - TypeSpecifierContext::createTruthy() - ); - } - - }, - new class() implements MethodTypeSpecifyingExtension, - TypeSpecifierAwareExtension { - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return \ImpossibleMethodCall\Foo::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context - ): bool - { - return $methodReflection->getName() === 'isSame' - && count($node->args) >= 2; - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context - ): SpecifiedTypes - { - return $this->typeSpecifier->specifyTypesInCondition( - $scope, - new \PhpParser\Node\Expr\BinaryOp\Identical( - $node->args[0]->value, - $node->args[1]->value - ), - TypeSpecifierContext::createTruthy() - ); - } - - }, - new class() implements MethodTypeSpecifyingExtension, - TypeSpecifierAwareExtension { - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return \ImpossibleMethodCall\Foo::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context - ): bool - { - return $methodReflection->getName() === 'isNotSame' - && count($node->args) >= 2; - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context - ): SpecifiedTypes - { - return $this->typeSpecifier->specifyTypesInCondition( - $scope, - new \PhpParser\Node\Expr\BinaryOp\NotIdentical( - $node->args[0]->value, - $node->args[1]->value - ), - TypeSpecifierContext::createTruthy() - ); - } - - }, - ]; - } - public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -201,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.', @@ -226,6 +78,89 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isSame() with stdClass and stdClass will always evaluate to true.', 78, ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with stdClass and stdClass will always evaluate to false.', + 81, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', + 101, + '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.', + 113, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{} and array{} will always evaluate to false.', + 116, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{1, 3} and array{1, 3} will always evaluate to true.', + 119, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{1, 3} and array{1, 3} will always evaluate to false.', + 122, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and stdClass will always evaluate to false.', + 126, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and stdClass will always evaluate to true.', + 130, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'1\' and stdClass will always evaluate to false.', + 133, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'1\' and stdClass will always evaluate to true.', + 136, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to false.', + 139, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to true.', + 142, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with stdClass and \'1\' will always evaluate to false.', + 145, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with stdClass and \'1\' will always evaluate to true.', + 148, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'\' and \'\' will always evaluate to true.', + 174, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'\' and \'\' will always evaluate to false.', + 175, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', + 191, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', + 194, + ], + [ + '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.', + ], ]); } @@ -264,4 +199,89 @@ 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 [ + __DIR__ . '/impossible-check-type-method-call.neon', + ]; + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 64079703c4..1cdbc1a7ad 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -2,29 +2,31 @@ namespace PHPStan\Rules\Comparison; -use PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension; -use PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeStaticMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - public function getRule(): \PHPStan\Rules\Rule + private bool $reportAlwaysTrueInLastCondition = false; + + public function getRule(): Rule { return new ImpossibleCheckTypeStaticMethodCallRule( new ImpossibleCheckTypeHelper( $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, true, - $this->treatPhpDocTypesAsCertain ); } @@ -33,17 +35,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - /** - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] - */ - protected function getStaticMethodTypeSpecifyingExtensions(): array - { - return [ - new AssertionClassStaticMethodTypeSpecifyingExtension(null), - new AssertStaticMethodTypeSpecifyingExtension(), - ]; - } - public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -72,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.', + ], ]); } @@ -110,4 +106,49 @@ 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 [ + __DIR__ . '/impossible-check-type-static-method-call.neon', + ]; + } + } 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 new file mode 100644 index 0000000000..d7a005c589 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -0,0 +1,520 @@ + + */ +class MatchExpressionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + private bool $reportAlwaysTrueInLastCondition = false; + + protected function getRule(): Rule + { + return new MatchExpressionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->reportAlwaysTrueInLastCondition, + $this->treatPhpDocTypesAsCertain, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + 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.', + 14, + ], + [ + 'Match arm comparison between 1|2|3 and 0 is always false.', + 19, + ], + [ + 'Match arm comparison between 3 and 3 is always true.', + 28, + $tipText, + ], + [ + 'Match arm comparison between 3 and 3 is always true.', + 35, + $tipText, + ], + [ + 'Match arm comparison between 1 and 1 is always true.', + 40, + $tipText, + ], + [ + 'Match arm comparison between 1 and 1 is always true.', + 46, + $tipText, + ], + [ + 'Match expression does not handle remaining value: 3', + 50, + ], + [ + 'Match arm comparison between 1|2 and 3 is always false.', + 61, + ], + [ + '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 + { + $this->analyse([__DIR__ . '/data/bug-5161.php'], []); + } + + public function testBug4857(): void + { + $this->analyse([__DIR__ . '/data/bug-4857.php'], [ + [ + 'Match expression does not handle remaining value: true', + 13, + ], + [ + 'Match expression does not handle remaining value: true', + 23, + ], + ]); + } + + public function testBug5454(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-5454.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/match-enums.php'], [ + [ + 'Match expression does not handle remaining values: MatchEnums\Foo::THREE|MatchEnums\Foo::TWO', + 19, + ], + [ + 'Match expression does not handle remaining values: MatchEnums\Foo::THREE|MatchEnums\Foo::TWO', + 35, + ], + [ + 'Match expression does not handle remaining value: MatchEnums\Foo::THREE', + 56, + ], + [ + 'Match arm comparison between 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, + ], + ]); + } + + public function testBug6394(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $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 aa828378d7..eb7fe43290 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -4,16 +4,27 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ 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 @@ -44,4 +55,201 @@ public function testBug2648Namespace(): void $this->analyse([__DIR__ . '/data/bug-2648-namespace-rule.php'], []); } + public function testBug5161(): void + { + $this->analyse([__DIR__ . '/data/bug-5161.php'], []); + } + + public function testBug3310(): void + { + $this->analyse([__DIR__ . '/data/bug-3310.php'], []); + } + + public function testBug3264(): void + { + $this->analyse([__DIR__ . '/data/bug-3264.php'], []); + } + + public function testBug5656(): void + { + $this->analyse([__DIR__ . '/data/bug-5656.php'], []); + } + + public function testBug3867(): void + { + $this->analyse([__DIR__ . '/data/bug-3867.php'], []); + } + + public function testIntegerRangeGeneralization(): void + { + $this->analyse([__DIR__ . '/data/integer-range-generalization.php'], []); + } + + public function testBug3153(): void + { + $this->analyse([__DIR__ . '/data/bug-3153.php'], []); + } + + public function testBug5707(): void + { + $this->analyse([__DIR__ . '/data/bug-5707.php'], []); + } + + public function testBug5969(): void + { + $this->analyse([__DIR__ . '/data/bug-5969.php'], []); + } + + 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 63605836b6..4c27bfd80f 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -2,23 +2,40 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Analyser\RicherScopeGetTypeHelper; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_INT_SIZE; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class StrictComparisonOfDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase +class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkAlwaysTrueStrictComparison; + private bool $reportAlwaysTrueInLastCondition = false; + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ); + } - protected function getRule(): \PHPStan\Rules\Rule + protected function shouldTreatPhpDocTypesAsCertain(): bool { - return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison); + 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'], [ @@ -45,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.', @@ -83,24 +101,26 @@ public function testStrictComparison(): void 130, ], [ - 'Strict comparison using === between array and null will always evaluate to false.', + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', 140, ], [ - 'Strict comparison using !== between 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 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.', @@ -111,31 +131,35 @@ public function testStrictComparison(): void 284, ], [ - 'Strict comparison using === between array(\'X\' => 1) and array(\'X\' => 2) will always evaluate to false.', + 'Strict comparison using === between array{X: 1} and array{X: 2} will always evaluate to false.', 292, ], [ - 'Strict comparison using === between array(\'X\' => 1, \'Y\' => 2) and array(\'X\' => 2, \'Y\' => 1) will always evaluate to false.', + 'Strict comparison using === between array{X: 1, Y: 2} and array{X: 2, Y: 1} will always evaluate to false.', 300, ], + [ + '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, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 335, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 343, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', 360, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 368, ], [ @@ -155,28 +179,25 @@ public function testStrictComparison(): void 426, ], [ - 'Strict comparison using === between int and null will always evaluate to false.', // todo remove with isDeterministic - 438, - ], - [ - 'Strict comparison using === between int<2, max>|int|string and 1.0 will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and 1.0 will always evaluate to false.', 464, ], [ - 'Strict comparison using === between int<2, max>|int|string and stdClass will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and stdClass will always evaluate to false.', 466, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', - 624, + 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', + 622, + $tipText, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', - 635, + 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', + 624, ], [ - 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', - 654, + 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', + 635, ], [ 'Strict comparison using === between string|null and 1 will always evaluate to false.', @@ -193,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.', @@ -226,151 +249,53 @@ 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 INF and INF will always evaluate to true.', + 979, ], [ - 'Strict comparison using === between 1 and 1.0 will always evaluate to false.', - 47, + 'Strict comparison using === between NAN and NAN will always evaluate to false.', + 980, ], [ - 'Strict comparison using === between string and null will always evaluate to false.', - 69, + 'Strict comparison using !== between INF and INF will always evaluate to false.', + 982, ], [ - 'Strict comparison using === between 1|2|3 and null will always evaluate to false.', - 98, + 'Strict comparison using !== between NAN and NAN will always evaluate to true.', + 983, ], [ - 'Strict comparison using === between array and null will always evaluate to false.', - 140, - ], - [ - 'Strict comparison using === between 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 and \'string\' will always evaluate to false.', - 335, - ], - [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', - 343, - ], - [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', - 360, - ], - [ - 'Strict comparison using === between int 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 int and null will always evaluate to false.', // todo remove with isDeterministic - 438, - ], - [ - 'Strict comparison using === between int<2, max>|int|string and 1.0 will always evaluate to false.', - 464, - ], - [ - 'Strict comparison using === between int<2, max>|int|string and stdClass will always evaluate to false.', - 466, + '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 int and \'foo\' will always evaluate to false.', - 624, + 'Strict comparison using === between lowercase-string|false and \'AB\' will always evaluate to false.', + 1014, + $tipText, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', - 635, - ], - [ - 'Strict comparison using === between string|null and 1 will always evaluate to false.', - 685, + '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.', - 695, + '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 string|null and 1 will always evaluate to false.', - 705, + '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.', ], - [ - 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', - 808, - ], - ] + ], ); } 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.', @@ -381,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.', @@ -407,8 +328,697 @@ public function testStrictComparisonPropertyNativeTypesPhp74(): void public function testBug2835(): void { - $this->checkAlwaysTrueStrictComparison = true; $this->analyse([__DIR__ . '/data/bug-2835.php'], []); } + public function testBug1860(): void + { + $this->analyse([__DIR__ . '/data/bug-1860.php'], [ + [ + 'Strict comparison using === between string and null will always evaluate to false.', + 15, + ], + [ + 'Strict comparison using !== between string and null will always evaluate to true.', + 19, + ], + ]); + } + + public function testBug3544(): void + { + $this->analyse([__DIR__ . '/data/bug-3544.php'], []); + } + + public function testBug2675(): void + { + $this->analyse([__DIR__ . '/data/bug-2675.php'], []); + } + + public function testBug2220(): void + { + $this->analyse([__DIR__ . '/data/bug-2220.php'], []); + } + + public function testBug1707(): void + { + $this->analyse([__DIR__ . '/data/bug-1707.php'], []); + } + + public function testBug3357(): void + { + $this->analyse([__DIR__ . '/data/bug-3357.php'], []); + } + + public function testBug4848(): void + { + if (PHP_INT_SIZE !== 8) { + $this->markTestSkipped('Test requires 64-bit platform.'); + } + $this->analyse([__DIR__ . '/data/bug-4848.php'], [ + [ + 'Strict comparison using === between \'18446744073709551615\' and \'9223372036854775807\' will always evaluate to false.', + 7, + ], + ]); + } + + public function testBug4793(): void + { + $this->analyse([__DIR__ . '/data/bug-4793.php'], []); + } + + public function testBug5062(): void + { + $this->analyse([__DIR__ . '/data/bug-5062.php'], []); + } + + public function testBug3366(): void + { + $this->analyse([__DIR__ . '/data/bug-3366.php'], []); + } + + public function testBug5362(): void + { + $this->analyse([__DIR__ . '/data/bug-5362.php'], [ + [ + 'Strict comparison using === between 0 and 1|2 will always evaluate to false.', + 23, + ], + ]); + } + + 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 9ed164b19a..e1e7474e17 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -2,16 +2,18 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class TernaryOperatorConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class TernaryOperatorConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new TernaryOperatorConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +21,12 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, + true, ); } @@ -44,6 +47,22 @@ public function testRule(): void 'Ternary operator condition is always false.', 15, ], + [ + 'Ternary operator condition is always false.', + 66, + ], + [ + 'Ternary operator condition is always false.', + 67, + ], + [ + 'Ternary operator condition is always true.', + 70, + ], + [ + 'Ternary operator condition is always true.', + 71, + ], ]); } @@ -74,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 66034184c4..0000000000 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ /dev/null @@ -1,119 +0,0 @@ - - */ -class UnreachableIfBranchesRuleTest extends RuleTestCase -{ - - /** @var bool */ - private $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 93c7d52bcc..0000000000 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ /dev/null @@ -1,94 +0,0 @@ - - */ -class UnreachableTernaryElseBranchRuleTest extends RuleTestCase -{ - - /** @var bool */ - private $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 new file mode 100644 index 0000000000..f0ec810999 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php @@ -0,0 +1,29 @@ + + */ +class UsageOfVoidMatchExpressionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UsageOfVoidMatchExpressionRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/void-match.php'], [ + [ + 'Result of match expression (void) is used.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php new file mode 100644 index 0000000000..4d65f02d2b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -0,0 +1,53 @@ + + */ +class WhileLoopAlwaysFalseConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new WhileLoopAlwaysFalseConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + true, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/while-loop-false.php'], [ + [ + 'While loop condition is always false.', + 10, + ], + [ + 'While loop condition is always false.', + 20, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php new file mode 100644 index 0000000000..4a377f1855 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -0,0 +1,57 @@ + + */ +class WhileLoopAlwaysTrueConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new WhileLoopAlwaysTrueConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + true, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/while-loop-true.php'], [ + [ + 'While loop condition is always true.', + 10, + ], + [ + 'While loop condition is always true.', + 20, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'While loop condition is always true.', + 65, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php new file mode 100644 index 0000000000..258f5251a9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php @@ -0,0 +1,241 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \PHPStan\Tests\AssertionClass::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'assertNotInt' + && count($node->args) > 0; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BooleanNot( + new \PhpParser\Node\Expr\FuncCall( + new \PhpParser\Node\Name('is_int'), + [ + $node->args[0], + ] + ) + ), + TypeSpecifierContext::createTruthy() + ); + } + +} + +class FooIsSame implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +{ + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\Identical( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} + +class FooIsNotSame implements MethodTypeSpecifyingExtension, + TypeSpecifierAwareExtension { + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isNotSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\NotIdentical( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} + +class FooIsEqual implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +{ + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\Equal( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} + +class FooIsNotEqual implements MethodTypeSpecifyingExtension, + TypeSpecifierAwareExtension { + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isNotSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\NotEqual( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php new file mode 100644 index 0000000000..1ba7c4855f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php @@ -0,0 +1,58 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \GenericTypeOverride\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'setFetchMode'; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + $newType = new GenericObjectType(\GenericTypeOverride\Foo::class, [new ObjectType(\GenericTypeOverride\Bar::class)]); + + return $this->typeSpecifier->create( + $node->var, + $newType, + TypeSpecifierContext::createTruthy(), + $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/array-is-list.php b/tests/PHPStan/Rules/Comparison/data/array-is-list.php new file mode 100644 index 0000000000..f812dc9a06 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/array-is-list.php @@ -0,0 +1,48 @@ + $stringKeyedArray + */ + public function doFoo(array $stringKeyedArray) + { + if (array_is_list($stringKeyedArray)) { + + } + } + + /** + * @param array $mixedArray + */ + public function doBar(array $mixedArray) + { + if (array_is_list($mixedArray)) { + // Fine + } + } + + /** + * @param array $arrayKeyedInts + */ + public function doBaz(array $arrayKeyedInts) + { + if (array_is_list($arrayKeyedInts)) { + // Fine + } + } + + public function doBax() + { + if (array_is_list(['foo' => 'bar', 'bar' => 'baz'])) { + + } + + if (array_is_list(['foo', 'foo' => 'bar', 'bar' => 'baz'])) { + // Fine + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/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 @@ + getMaybeArray(), + 'b' => getMaybeArray(), + ]; + + if (isset($arr['a']) && isset($arr['b'])) { + } +} + +class ConditionalAlwaysTrue +{ + public function sayHello(int $i): void + { + $one = 1; + if ($i < 5) { + } elseif ($one && $i) { // always-true should not be reported because last condition + } + + if ($i < 5) { + } elseif ($one && $i) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } +} + +class Foo +{ + +} + +class Bar +{ + +} + +interface Lorem +{ + +} + +interface Ipsum +{ + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-logical-and.php b/tests/PHPStan/Rules/Comparison/data/boolean-logical-and.php new file mode 100644 index 0000000000..d70f4f4a49 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-logical-and.php @@ -0,0 +1,186 @@ +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-1613.php b/tests/PHPStan/Rules/Comparison/data/bug-1613.php new file mode 100644 index 0000000000..563f7d93de --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-1613.php @@ -0,0 +1,14 @@ + "test" + ]; + return array_key_exists($index, $array); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-1707.php b/tests/PHPStan/Rules/Comparison/data/bug-1707.php new file mode 100644 index 0000000000..bc7a527f01 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-1707.php @@ -0,0 +1,23 @@ + 1, 'b' => 2]; + $keys = ['a', 'b', 'c', 'd']; + + foreach ($keys as $key) { + if(array_key_exists($key, $values)){ + unset($values[$key]); + } + + if(0 === \count($values)) { + break; + } + } + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-1746.php b/tests/PHPStan/Rules/Comparison/data/bug-1746.php new file mode 100644 index 0000000000..c6b36ee3a7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-1746.php @@ -0,0 +1,30 @@ + ['blah' => 'boohoo']]; + $assocModel = 'foo'; + $parents = ['Class' => ['foo' => 'bar', 'bar' => 'baz', 'foreignKey' => 'blah']]; + + // initial value + $isMatch = true; + foreach ($parents as $parentModel) { + $fk = $parentModel['foreignKey']; + if (isset($data[$fk])) { + // redetermine whether $isMatch is still true + $isMatch = $isMatch && ($data[$fk] == $existing[$assocModel][$fk]); + + // bail + if (!$isMatch) { + break; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-1860.php b/tests/PHPStan/Rules/Comparison/data/bug-1860.php new file mode 100644 index 0000000000..beb2714bbf --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-1860.php @@ -0,0 +1,24 @@ +doFoo() === null) { + echo 'foo'; + } + + if ($this->doFoo() !== null) { + echo 'bar'; + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2220.php b/tests/PHPStan/Rules/Comparison/data/bug-2220.php new file mode 100644 index 0000000000..1db5e4fbcc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2220.php @@ -0,0 +1,26 @@ +getResource(); + + if ($resource === "{$this->privateModule}:abcdef") { + $this->abc(); + } elseif ($resource === "{$this->privateModule}:xyz") { + $this->abc(); + } + } + + private function abc(): void {} + + private function getResource(): string { return 'string'; } +} 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 @@ + str_repeat('a', rand(1, 10)), + ]; + } + + $list[] = [ + 'type' => 'x', + ]; + + foreach ($list as $item) { + if (in_array($item['type'], ['aaa', 'aaaa'], TRUE)) { + echo 'OK'; + } + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2741-or.php b/tests/PHPStan/Rules/Comparison/data/bug-2741-or.php new file mode 100644 index 0000000000..1acfd6ee2c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2741-or.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-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 @@ + 10 ){ + break; + } + } + } + } + + public function doBar() + { + $rows = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]; + $added_rows = 0; + $limit = random_int(1, 20); + + foreach($rows as $row){ + + if( $added_rows >= $limit ){ + break; + } + $added_rows++; + } + + if( $added_rows < 3 ){ + foreach($rows as $row){ + + $added_rows++; + + if( $added_rows > 10 ){ + break; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3264.php b/tests/PHPStan/Rules/Comparison/data/bug-3264.php new file mode 100644 index 0000000000..34fb93f0f2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3264.php @@ -0,0 +1,17 @@ + '']; + + assert($foo === ['foo' => '']); +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3366.php b/tests/PHPStan/Rules/Comparison/data/bug-3366.php new file mode 100644 index 0000000000..e7076b6c50 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3366.php @@ -0,0 +1,26 @@ + '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-3544.php b/tests/PHPStan/Rules/Comparison/data/bug-3544.php new file mode 100644 index 0000000000..3b3184b494 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3544.php @@ -0,0 +1,22 @@ + $input + */ + public function foo(array $input): void + { + if( ! array_key_exists( 'foo', $input)) { + throw new \LogicException(); + } + + unset($input['foo']); + + if($input === []) { + echo 'hello'; + } + } +} 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-3766.php b/tests/PHPStan/Rules/Comparison/data/bug-3766.php new file mode 100644 index 0000000000..5058948f13 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3766.php @@ -0,0 +1,28 @@ + + */ + function get_foo(): array + { + return []; + } + + public function doFoo(): void + { + $foo = $this->get_foo(); + for ($i = 0; $i < \count($foo); $i++) { + if (\array_key_exists($i + 1, $foo) + && \array_key_exists($i + 2, $foo) + ) { + echo $i; + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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 @@ +$method(); + + if (!empty($result)) { + break; + } + } while (count($try) > 0); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3892.php b/tests/PHPStan/Rules/Comparison/data/bug-3892.php new file mode 100644 index 0000000000..8148f7533a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3892.php @@ -0,0 +1,72 @@ + '200', ReceiptOrder::class => '300']; + + /** @var Order[] */ + private $dtos; + + public function __construct(OrderEntity $order) + { + $this->dtos = \array_filter([ReceiptOrder::fromOrder($order), PickingOrder::fromOrder($order)]); + } + + + /** + * @return Order[] + */ + public function getDTOs(): array + { + return $this->dtos; + } +} + +abstract class Order +{ + public const TYP = [PickingOrder::class => '200', ReceiptOrder::class => '300']; +} + +class PickingOrder extends Order +{ + public static function fromOrder(OrderEntity $order): ?self + { + return $order->isLoaded() ? new self() : null; + } +} + +class ReceiptOrder extends Order +{ + public static function fromOrder(OrderEntity $order): ?self + { + return $order->isLoaded() ? new self() : null; + } +} + +class Foo +{ + + public function doFoo(OrderSaved $event) + { + $DTOs = $event->getDTOs(); + + $DTOClasses = \array_map('\get_class', $DTOs); + $missingClasses = \array_diff(\array_keys(Order::TYP), $DTOClasses); + + if (\in_array(ReceiptOrder::class, $missingClasses, true)) { + + } + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3979.php b/tests/PHPStan/Rules/Comparison/data/bug-3979.php new file mode 100644 index 0000000000..f0f21220d1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3979.php @@ -0,0 +1,130 @@ +diff(new \DateTime()); + assert($diff !== false); + + $mdiff = (int)$diff->format('%m'); + $ddiff = (int)$diff->format('%d'); + $hdiff = (int)$diff->format('%H'); + $idiff = (int)$diff->format('%i'); + + $m = $mdiff ? number($mdiff, 'месяц|месяца|месяцев') : ''; + $d = $ddiff ? number($ddiff, 'день|дня|дней') : ''; + $h = $hdiff ? number($hdiff, 'час|часа|часов') : ''; + $i = $idiff ? number($idiff, 'минута|минуты|минут') : ''; + + $content = array_filter([$m, $d, $h, $i]); + if ($content) { + echo 1; + } +}; + +function (): void { + $a = []; + if (rand(0, 1)) { + $a[] = 1; + } + if ($a) { + + } +}; + +function (): void { + $a = []; + if ($a) { + + } +}; + +function (): void { + $a = [1]; + if ($a) { + + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4061.php b/tests/PHPStan/Rules/Comparison/data/bug-4061.php new file mode 100644 index 0000000000..deb24430ba --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4061.php @@ -0,0 +1,25 @@ += 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-4657.php b/tests/PHPStan/Rules/Comparison/data/bug-4657.php new file mode 100644 index 0000000000..f01f87be15 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4657.php @@ -0,0 +1,15 @@ + $objects */ + public function test(array $objects): void + { + $types = []; + foreach ($objects as $object) { + if (self::CONST_1 === $object->getType() && !in_array(self::CONST_2, $types, true)) { + $types[] = self::CONST_2; + } + } + } +} + +class MyObject +{ + /** @var string */ + private $type; + public function getType(): string{ + return $this->type; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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-4793.php b/tests/PHPStan/Rules/Comparison/data/bug-4793.php new file mode 100644 index 0000000000..c0d043061a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4793.php @@ -0,0 +1,41 @@ += 8.0 + +namespace Bug4857; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + function format(int $seconds): void + { + $minutes = (int) \round($seconds / 60); + match(true) { + $minutes < 60 => assertType('int', $minutes), + $minutes < 90 => assertType('int<60, 89>', $minutes), + $minutes < 150 => assertType('int<90, 149>', $minutes), + }; + } + + function format2(int $seconds): void + { + $minutes = (int) \round($seconds / 60); + match(true) { + $minutes <= 60 => assertType('int', $minutes), + $minutes <= 90 => assertType('int<61, 90>', $minutes), + $minutes <= 150 => assertType('int<91, 150>', $minutes), + }; + } + + public function sayHello(): void + { + /** @var int */ + $x = 5; // int + + match(true) { + $x < 60 => assertType('int', $x), + $x < 90 => assertType('int<60, 89>', $x), + default => assertType('int<90, max>', $x), + }; + } + +} 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-5062.php b/tests/PHPStan/Rules/Comparison/data/bug-5062.php new file mode 100644 index 0000000000..ab7d67af32 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5062.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug5161; + +final class Log { + + public int $a; + + public function test(int $i): void + { + $this->a = match (true) { + $i >= 30 => 30, + $i >= 20 => 20, + $i >= 10 => 10, + default => 0, + }; + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5295.php b/tests/PHPStan/Rules/Comparison/data/bug-5295.php new file mode 100644 index 0000000000..4818153a87 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5295.php @@ -0,0 +1,23 @@ +getValue() as $key => $val) { + if ($key >= 5 && $key <= 10) { + } elseif ($key > 10 && $key <= 15) { + } else { + } + } + } + + /** + * @return array + */ + public function getValue(): array { + return []; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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 @@ + 10 ? 0 : 1; + } + + if (\in_array(0, $a, true)) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5362.php b/tests/PHPStan/Rules/Comparison/data/bug-5362.php new file mode 100644 index 0000000000..51284284ca --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5362.php @@ -0,0 +1,32 @@ +doFoo($retry); + + break; + } catch (\Exception $e) { + if (0 === $retry) { + throw $e; + } + + --$retry; + } + } while ($retry > 0); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5365.php b/tests/PHPStan/Rules/Comparison/data/bug-5365.php new file mode 100644 index 0000000000..54f2263446 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5365.php @@ -0,0 +1,22 @@ +\d+)$#i'; + $subject = 'C 1234567890'; + + $found = (bool)preg_match( $pattern, $subject, $matches ) && isset( $matches['productId'] ); + assertType('bool', $found); +}; + +function (): void { + $matches = []; + $pattern = '#^C\s+(?\d+)$#i'; + $subject = 'C 1234567890'; + + assertType('bool', preg_match( $pattern, $subject, $matches ) ? isset( $matches['productId'] ) : false); +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5369.php b/tests/PHPStan/Rules/Comparison/data/bug-5369.php new file mode 100644 index 0000000000..84707aa8ad --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5369.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug5454; + +class TextFormat{ + public const ESCAPE = "\xc2\xa7"; //§ + + public const BLACK = TextFormat::ESCAPE . "0"; + public const DARK_BLUE = TextFormat::ESCAPE . "1"; + public const DARK_GREEN = TextFormat::ESCAPE . "2"; + public const DARK_AQUA = TextFormat::ESCAPE . "3"; + public const DARK_RED = TextFormat::ESCAPE . "4"; + public const DARK_PURPLE = TextFormat::ESCAPE . "5"; + + public const OBFUSCATED = TextFormat::ESCAPE . "k"; + public const BOLD = TextFormat::ESCAPE . "l"; + public const STRIKETHROUGH = TextFormat::ESCAPE . "m"; + public const UNDERLINE = TextFormat::ESCAPE . "n"; + public const ITALIC = TextFormat::ESCAPE . "o"; + public const RESET = TextFormat::ESCAPE . "r"; +} + +class Terminal +{ + + /** + * @param string[] $string + */ + public static function toANSI(array $string) : string{ + $newString = ""; + foreach($string as $token){ + $newString .= match ($token){ + TextFormat::BOLD => "bold", + TextFormat::OBFUSCATED => "obf", + TextFormat::ITALIC => "italic", + TextFormat::UNDERLINE => "underline", + TextFormat::STRIKETHROUGH => "strike", + TextFormat::RESET => "reset", + TextFormat::BLACK => "black", + TextFormat::DARK_BLUE => "blue", + TextFormat::DARK_GREEN => "green", + TextFormat::DARK_AQUA => "aqua", + TextFormat::DARK_RED => "red", + default => $token, + }; + } + + return $newString; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5474.php b/tests/PHPStan/Rules/Comparison/data/bug-5474.php new file mode 100644 index 0000000000..14cf8a14ed --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5474.php @@ -0,0 +1,36 @@ + 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-5496.php b/tests/PHPStan/Rules/Comparison/data/bug-5496.php new file mode 100644 index 0000000000..dc5f9c6f77 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5496.php @@ -0,0 +1,32 @@ + $propagation + */ + public function propagate($propagation): void + { + } +} + +class Foo +{ + + public function doFoo() + { + $type = new ConstParamTypes(); + + /** @var array $propagation */ + $propagation = []; + + if (\in_array('auto', $propagation, true)) { + $type->propagate($propagation); + } + + $type->propagate(['yakdam' => 'copy']); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5656.php b/tests/PHPStan/Rules/Comparison/data/bug-5656.php new file mode 100644 index 0000000000..aca016dfbb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5656.php @@ -0,0 +1,41 @@ + 10) { + $i = 1; + } + $control += $i * $v; + ++$i; + } + + $control %= 11; + + if (10 !== $control) { + break; + } + } + + if (10 === $control) { + $control = 0; + } + + return $expected === $control; +} + +$values = [0, 1, 0, 6, 0, 6, 2, 3, 1, 5]; +okpoValidate($values); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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 @@ + 0; --$l) { + } + + return 'x'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5743.php b/tests/PHPStan/Rules/Comparison/data/bug-5743.php new file mode 100644 index 0000000000..d693bbdf47 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5743.php @@ -0,0 +1,10 @@ +date)) return; + + if (strtotime($o->date) < time()) echo "a"; + + // surprisingly this is not an issue + if (strtotime($this->date) < time()) echo "b"; + + if (is_string($o->date) && strtotime($o->date) < time()) echo "c"; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5978.php b/tests/PHPStan/Rules/Comparison/data/bug-5978.php new file mode 100644 index 0000000000..aa312df62f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5978.php @@ -0,0 +1,12 @@ + 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 Bug6258; + +defined('a') || die(); +defined('a') or die(); +rand() === rand() || die(); + + +defined('a') || exit(); +defined('a') or exit(); +rand() === rand() || exit(); + + +defined('a') || throw new \Exception(''); +defined('a') or throw new \Exception(''); +rand() === rand() || throw new \Exception(''); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6260.php b/tests/PHPStan/Rules/Comparison/data/bug-6260.php new file mode 100644 index 0000000000..dd5d1eb1f0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6260.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug6260; + +class Foo{ + public function __construct( + /** @var non-empty-array */ + private array $array + ){ + if(count($array) === 0){ + throw new \InvalidArgumentException(); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6305.php b/tests/PHPStan/Rules/Comparison/data/bug-6305.php new file mode 100644 index 0000000000..c4d142a276 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6305.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug6394; + +enum EntryType: string +{ + case CREDIT = 'credit'; + case DEBIT = 'debit'; +} + +class Foo +{ + + public function getType(): EntryType + { + return $this->type; + } + + public function getAmount(): int + { + return match($this->getType()) { + EntryType::DEBIT => 1, + EntryType::CREDIT => 2, + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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 new file mode 100644 index 0000000000..fba7f6a8be --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6473.php @@ -0,0 +1,48 @@ +visited = true; + assertType('true', $p->visited); + $seen = [ + ... $seen, + ... array_filter( $p->getNeighbours(), static fn (Point $p) => !$p->visited ) + ]; + assertType('true', $p->visited); + } + } + + public function doFoo2() + { + $seen = []; + + foreach([new Point, new Point] as $p ) { + + $p->visited = true; + assertType('true', $p->visited); + $seen = [ + ... $seen, + ... array_filter( $p->getNeighbours(), static fn (Point $p2) => !$p2->visited ) + ]; + assertType('true', $p->visited); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6551.php b/tests/PHPStan/Rules/Comparison/data/bug-6551.php new file mode 100644 index 0000000000..561fbc9cfd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6551.php @@ -0,0 +1,63 @@ + 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + $match = []; + if (false === preg_match('/^c(\d+)$/', $key, $match) || empty($match)) { + continue; + } + var_dump($key); + var_dump($value); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + if (false === preg_match('/^c(\d+)$/', $key, $match) || empty($match)) { + continue; + } + var_dump($key); + var_dump($value); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + $match = []; + assertType('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 @@ + + */ + public function getClasses(): iterable; +} + +class Y +{ + /** @var X */ + public $x; + + /** + * @template T of object + * + * @param class-string $type + * @return iterable> + */ + public function findImplementations(string $type): iterable + { + foreach ($this->x->getClasses() as $class) { + if (is_subclass_of($class, $type)) { + yield $class; + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6776.php b/tests/PHPStan/Rules/Comparison/data/bug-6776.php new file mode 100644 index 0000000000..f05a9ad1ee --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6776.php @@ -0,0 +1,16 @@ + 1, 'b' => 2]; + /** @var array * */ + $array2 = ['a' => 1]; + + $check = function (string $key) use (&$array1, &$array2): bool { + if (!isset($array1[$key], $array2[$key])) { + return false; + } + // ... more conditions here ... + return true; + }; + + if ($check('a')) { + // ... + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6938.php b/tests/PHPStan/Rules/Comparison/data/bug-6938.php new file mode 100644 index 0000000000..1f14920582 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6938.php @@ -0,0 +1,22 @@ + $value + */ +function myFunction($value): void +{ + if (is_string($value)) { + $value = [$value]; + } elseif (is_array($value)) { + // If given an array, filter out anything that isn't a string. + $value = array_filter($value, 'is_string'); + } + + if (! is_array($value)) { + throw new \DomainException('Invalid argument type for $value'); + } + + // Now we know that $value is either a string or an array of strings. +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-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-composer-dependent-variables.php b/tests/PHPStan/Rules/Comparison/data/bug-composer-dependent-variables.php new file mode 100644 index 0000000000..e289c029b5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-composer-dependent-variables.php @@ -0,0 +1,22 @@ += 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/do-while-loop.php b/tests/PHPStan/Rules/Comparison/data/do-while-loop.php new file mode 100644 index 0000000000..02eb303d34 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/do-while-loop.php @@ -0,0 +1,98 @@ +createGenericFoo(); + assertType('Foo', $foo); + + // $foo generic will be overridden via MethodTypeSpecifyingExtension + $foo->setFetchMode(); + assertType('Foo', $foo); + } + + /** + * @return Foo + */ + public function createGenericFoo() { + + } +} + + +/** + * @template T + */ +class Foo +{ + public function setFetchMode() { + + } +} + + +class Bar +{ +} diff --git a/tests/PHPStan/Rules/Comparison/data/hashing.php b/tests/PHPStan/Rules/Comparison/data/hashing.php new file mode 100644 index 0000000000..14fad8b550 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/hashing.php @@ -0,0 +1,34 @@ +isSame(self::createStdClass('a'), self::createStdClass('a'))) { + + } + if ($this->isNotSame(self::createStdClass('b'), self::createStdClass('b'))) { + + } + if ($this->isSame(self::returnFoo('a'), self::returnFoo('a'))) { + + } + if ($this->isNotSame(self::returnFoo('b'), self::returnFoo('b'))) { + + } + if ($this->isSame(self::createStdClass('a')->foo, self::createStdClass('a')->foo)) { + + } + if ($this->isNotSame(self::createStdClass('b')->foo, self::createStdClass('b')->foo)) { + + } + if ($this->isSame([], [])) { + + } + if ($this->isNotSame([], [])) { + + } + if ($this->isSame([1, 3], [1, 3])) { + + } + if ($this->isNotSame([1, 3], [1, 3])) { + + } + $std3 = new \stdClass(); + if ($this->isSame(1, $std3)) { + + } + $std4 = new \stdClass(); + if ($this->isNotSame(1, $std4)) { + + } + if ($this->isSame('1', new \stdClass())) { + + } + if ($this->isNotSame('1', new \stdClass())) { + + } + if ($this->isSame(['a', 'b'], [1, 2])) { + + } + if ($this->isNotSame(['a', 'b'], [1, 2])) { + + } + if ($this->isSame(new \stdClass(), '1')) { + + } + if ($this->isNotSame(new \stdClass(), '1')) { + + } } public function nullableInt(): ?int @@ -99,4 +155,64 @@ public function nullableInt(): ?int } + public static function createStdClass(string $foo): \stdClass + { + return new \stdClass(); + } + + /** + * @return 'foo' + */ + public static function returnFoo(string $foo): string + { + return 'foo'; + } + + public function nonEmptyString() + { + $s = ''; + $this->isSame($s, ''); + $this->isNotSame($s, ''); + } + + public function stdClass(\stdClass $a) + { + $this->isSame($a, new \stdClass()); + } + + public function stdClass2(\stdClass $a) + { + $this->isNotSame($a, new \stdClass()); + } + + public function scalars() + { + $i = 1; + $this->isSame($i, 1); + + $j = 2; + $this->isNotSame($j, 2); + } + +} + +class ConditionalAlwaysTrue +{ + public function sayHello(?int $date): void + { + if ($date === null) { + } elseif ($this->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 @@ + $v) { + $result[$k] = $dt->format('d'); + } + + $d = new \DateTimeImmutable(); + if (in_array($d->format('d'), $result, true)) { + + } + + if (in_array('01', $result, true)) { + + } + + $day = $d->format('d'); + if (rand(0, 1)) { + $day = '32'; + } + + if (in_array($day, $result, true)) { + + } + } + + /** + * @param non-empty-array $a + */ + public function doBar(array $a, int $i) + { + if (in_array('a', $a, true)) { + + } + + if (in_array('b', $a, true)) { + + } + + if (in_array($i, [], true)) { + + } + } + + /** + * @param array $a + */ + public function doBaz(array $a, int $i, string $s) + { + if (in_array($s, $a, true)) { + + } + + if (in_array($i, $a, true)) { + + } + } + + /** + * @param non-empty-array $a + */ + public function doLorem(array $a, int $i) + { + if (in_array('a', $a, true)) { + + } + + if (in_array('b', $a, true)) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php b/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php new file mode 100644 index 0000000000..c79e6e2106 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php @@ -0,0 +1,28 @@ += 8.1 + +namespace 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 new file mode 100644 index 0000000000..43e765c552 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/match-enums.php @@ -0,0 +1,128 @@ += 8.1 + +namespace MatchEnums; + +enum Foo: int +{ + + case ONE = 1; + case TWO = 2; + case THREE = 3; + + public function returnStatic(): static + { + return $this; + } + + public function doFoo(): string + { + return match ($this->returnStatic()) { + self::ONE => 'one', + }; + } + + public function doBar(): string + { + return match ($this->returnStatic()) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doBaz(): string + { + return match ($this) { + self::ONE => 'one', + }; + } + + public function doIpsum(): string + { + return match ($this) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + +} + +class Bar +{ + + public function doFoo(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::TWO => 'two', + }; + } + + public function doBar(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doBaz(Foo $foo, Foo $bar): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + $bar => 'four', + }; + } + + 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 new file mode 100644 index 0000000000..5970804bb4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -0,0 +1,217 @@ += 8.0 + +namespace MatchExprRule; + +class Foo +{ + + /** + * @param 1|2|3 $i + */ + public function doFoo(int $i): void + { + match ($i) { + 'foo' => null, // always false + default => null, + }; + + match ($i) { + 0 => null, + 1 => null, + 2 => null, + 3 => null, // always true, but do not report (it's the last one) + }; + + match ($i) { + 1 => null, + 2 => null, + 3 => null, // always true - report with strict-rules + 4 => null, // unreachable + }; + + match ($i) { + 1 => null, + 2 => null, + 3 => null, // always true - report with strict-rules + default => null, // unreachable + }; + + match (1) { + 1 => null, // always true - report with strict-rules + 2 => null, // unreachable + 3 => null, // unreachable + }; + + match (1) { + 1 => null, // always true - report with strict-rules + default => null, // unreachable + }; + + match ($i) { + 1, 2 => null, + // unhandled + }; + + match ($i) { + 1, 2 => null, + default => null, // OK + }; + + match ($i) { + 3, 3 => null, // second 3 is always false + default => null, + }; + + match (1) { + 1 => 1, + }; + + match ($i) { + default => 1, + }; + + match ($i) { + default => 1, + 1 => 2, + }; + + match ($i) { + // unhandled + }; + } + + public function doBar(\Exception $e): void + { + match (true) { + $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule + default => null, + }; + + match (true) { + $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule + }; + } + + /** + * @param \stdClass&\Exception $obj + */ + public function doBaz($obj): void + { + match ($obj) { + + }; + } + + public function doFooConstants(int $i): void + { + + } + +} + +class BarConstants +{ + + const TEST1 = 1; + const TEST2 = 2; + + /** + * @param BarConstants::TEST1|BarConstants::TEST2 $i + */ + public function doFoo(int $i): void { + match ($i) { + BarConstants::TEST1 => 'foo', + BarConstants::TEST2 => 'bar', + }; + } + + /** + * @param BarConstants::TEST* $i + */ + public function doBar(int $i): void { + match ($i) { + BarConstants::TEST1 => 'foo', + BarConstants::TEST2 => 'bar', + }; + } + + +} + +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 @@ -440,7 +440,7 @@ public function doFoo() } } } - + /** @phpstan-impure */ public function nullableInt(): ?int { @@ -949,3 +949,105 @@ public function test(int $key) } } } + +class ArrayWithNonEmptyStringValue +{ + + /** + * @param array $a + */ + public function doFoo(array $a): void + { + if ($a === []) { + + } + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + if ($a === []) { + + } + } + +} + +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 e9c670d4fa..b693f44656 100644 --- a/tests/PHPStan/Rules/Comparison/data/ternary.php +++ b/tests/PHPStan/Rules/Comparison/data/ternary.php @@ -1,6 +1,6 @@ = 8.0 + +namespace VoidMatch; + +class Foo +{ + + public function doFoo(): void + { + + } + + public function doBar(int $i): void + { + match ($i) { + 1 => $this->doFoo(), + 2 => $this->doFoo(), + default => $this->doFoo(), + }; + + $a = match ($i) { + 1 => $this->doFoo(), + 2 => $this->doFoo(), + default => $this->doFoo(), + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/while-loop-false.php b/tests/PHPStan/Rules/Comparison/data/while-loop-false.php new file mode 100644 index 0000000000..667156e7c5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/while-loop-false.php @@ -0,0 +1,35 @@ + + */ +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 a3294f4610..0da9819e08 100644 --- a/tests/PHPStan/Rules/Constants/ConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantRuleTest.php @@ -2,15 +2,24 @@ namespace PHPStan\Rules\Constants; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use function define; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ConstantRuleTest extends \PHPStan\Testing\RuleTestCase +class ConstantRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ConstantRule(); + return new ConstantRule(true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testConstants(): void @@ -22,15 +31,13 @@ public function testConstants(): void [ 'Constant NONEXISTENT_CONSTANT not found.', 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Constant DEFINED_CONSTANT not found.', - 13, - ], - /*[ 'Constant DEFINED_CONSTANT_IF not found.', 21, - ],*/ + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], ]); } @@ -45,6 +52,8 @@ public function testCompilerHaltOffsetConstantIsUndefinedDetection(): void [ 'Constant __COMPILER_HALT_OFFSET__ not found.', 3, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], ]); } @@ -59,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/DirectAlwaysUsedClassConstantsExtensionProvider.php b/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php new file mode 100644 index 0000000000..861c72d7ab --- /dev/null +++ b/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php @@ -0,0 +1,23 @@ +extensions; + } + +} 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 new file mode 100644 index 0000000000..1fac7d6798 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php @@ -0,0 +1,51 @@ + + */ +class FinalConstantRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new FinalConstantRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80000, + [ + [ + 'Final class constants are supported only on PHP 8.1 and later.', + 9, + ], + ], + ], + [ + 80100, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param list $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + $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 new file mode 100644 index 0000000000..13e745a3d1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -0,0 +1,67 @@ + + */ +class MissingClassConstantTypehintRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingClassConstantTypehintRule(new MissingTypehintCheck(true, [])); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/missing-class-constant-typehint.php'], [ + [ + 'Constant MissingClassConstantTypehint\Foo::BAR type has no value type specified in iterable type array.', + 11, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Constant MissingClassConstantTypehint\Foo::BAZ with generic class MissingClassConstantTypehint\Bar does not specify its types: T', + 17, + ], + [ + 'Constant MissingClassConstantTypehint\Foo::LOREM type has no signature specified for callable.', + 20, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..6ce51b0297 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php @@ -0,0 +1,119 @@ + */ +class OverridingConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OverridingConstantRule(true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/overriding-constant.php'], [ + [ + 'Type string of constant OverridingConstant\Bar::BAR is not covariant with type int of constant OverridingConstant\Foo::BAR.', + 30, + ], + [ + 'Type int|string of constant OverridingConstant\Bar::IPSUM is not covariant with type int of constant OverridingConstant\Foo::IPSUM.', + 39, + ], + ]); + } + + public function testFinal(): void + { + $errors = [ + [ + 'Constant OverridingFinalConstant\Bar::FOO overrides final constant OverridingFinalConstant\Foo::FOO.', + 18, + ], + [ + 'Constant OverridingFinalConstant\Bar::BAR overrides final constant OverridingFinalConstant\Foo::BAR.', + 19, + ], + ]; + + if (PHP_VERSION_ID < 80100) { + $errors[] = [ + 'Constant OverridingFinalConstant\Baz::FOO overrides final constant OverridingFinalConstant\FooInterface::FOO.', + 34, + ]; + } + + $errors[] = [ + 'Constant OverridingFinalConstant\Baz::BAR overrides final constant OverridingFinalConstant\FooInterface::BAR.', + 35, + ]; + + if (PHP_VERSION_ID < 80100) { + $errors[] = [ + 'Constant OverridingFinalConstant\Lorem::FOO overrides final constant OverridingFinalConstant\BarInterface::FOO.', + 51, + ]; + } + + $errors[] = [ + 'Type string of constant OverridingFinalConstant\Lorem::FOO is not covariant with type int of constant OverridingFinalConstant\BarInterface::FOO.', + 51, + ]; + + $errors[] = [ + 'Private constant OverridingFinalConstant\PrivateDolor::PROTECTED_CONST overriding protected constant OverridingFinalConstant\Dolor::PROTECTED_CONST should be protected or public.', + 69, + ]; + $errors[] = [ + 'Private constant OverridingFinalConstant\PrivateDolor::PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::PUBLIC_CONST should also be public.', + 70, + ]; + $errors[] = [ + 'Private constant OverridingFinalConstant\PrivateDolor::ANOTHER_PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::ANOTHER_PUBLIC_CONST should also be public.', + 71, + ]; + $errors[] = [ + 'Protected constant OverridingFinalConstant\ProtectedDolor::PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::PUBLIC_CONST should also be public.', + 80, + ]; + $errors[] = [ + 'Protected constant OverridingFinalConstant\ProtectedDolor::ANOTHER_PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::ANOTHER_PUBLIC_CONST should also be public.', + 81, + ]; + + $this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); + } + + 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.1 + +namespace FinalConstant; + +class Foo +{ + + const TEST = 1; + final const BAR = 2; + +} diff --git a/tests/PHPStan/Rules/Constants/data/final-private-const.php b/tests/PHPStan/Rules/Constants/data/final-private-const.php new file mode 100644 index 0000000000..23c21b1271 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/final-private-const.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/overriding-constant.php b/tests/PHPStan/Rules/Constants/data/overriding-constant.php new file mode 100644 index 0000000000..de893d67e6 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/overriding-constant.php @@ -0,0 +1,41 @@ += 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 new file mode 100644 index 0000000000..40711affec --- /dev/null +++ b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php @@ -0,0 +1,59 @@ + + */ +class DateTimeInstantiationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DateTimeInstantiationRule(); + } + + public function test(): void + { + $this->analyse( + [__DIR__ . '/data/datetime-instantiation.php'], + [ + [ + 'Instantiating DateTime with 2020.11.17 produces an error: Double time specification', + 3, + ], + /*[ + 'Instantiating DateTimeImmutable with asdfasdf produces a warning: Double timezone specification', + 5, + ],*/ + [ + 'Instantiating DateTimeImmutable with asdfasdf produces an error: The timezone could not be found in the database', + 5, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.17 produces an error: Double time specification', + 10, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.18 produces an error: Double time specification', + 17, + ], + /*[ + '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 3d6b7fc4f8..2e082297d1 100644 --- a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php @@ -2,19 +2,20 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class NoopRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new NoopRule(new Standard()); + return new NoopRule(new ExprPrinter(new Printer())); } public function testRule(): void @@ -76,7 +77,80 @@ 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 + { + $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [ + [ + 'Expression "$ref?->name" on a separate line does not do anything.', + 10, + ], + ]); + } + + public function testRuleImpurePoints(): void + { + $this->analyse([__DIR__ . '/data/noop-impure-points.php'], [ + [ + 'Unused result of "&&" operator.', + 12, + ], + [ + 'Expression "$b()" on a separate line does not do anything.', + 59, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 98, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 104, + ], ]); } + 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 9d7e4faabf..ec97b0481a 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -6,13 +6,12 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class UnreachableStatementRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; protected function getRule(): Rule { @@ -42,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, ], ]); } @@ -72,7 +83,6 @@ public function dataBugWithoutGitHubIssue1(): array /** * @dataProvider dataBugWithoutGitHubIssue1 - * @param bool $treatPhpDocTypesAsCertain */ public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): void { @@ -80,4 +90,155 @@ public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): voi $this->analyse([__DIR__ . '/data/bug-without-issue-1.php'], []); } + public function testBug4070(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4070.php'], []); + } + + public function testBug4070Two(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4070_2.php'], []); + } + + public function testBug4076(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4076.php'], []); + } + + public function testBug4535(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4535.php'], []); + } + + public function testBug4346(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4346.php'], []); + } + + public function testBug2913(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2913.php'], []); + } + + public function testBug4370(): void + { + $this->treatPhpDocTypesAsCertain = true; + $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 new file mode 100644 index 0000000000..9ef9924063 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -0,0 +1,115 @@ + + */ +class UnusedPrivateConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UnusedPrivateConstantRule( + new DirectAlwaysUsedClassConstantsExtensionProvider([ + new class() implements AlwaysUsedClassConstantsExtension { + + public function isAlwaysUsed(ClassConstantReflection $constant): bool + { + return $constant->getDeclaringClass()->getName() === TestExtension::class + && $constant->getName() === 'USED'; + } + + }, + ]), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/unused-private-constant.php'], [ + [ + 'Constant UnusedPrivateConstant\Foo::BAR_CONST is unused.', + 10, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + [ + 'Constant UnusedPrivateConstant\TestExtension::UNUSED is unused.', + 23, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + + public function testBug5651(): void + { + $this->analyse([__DIR__ . '/data/bug-5651.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/unused-private-constant-enum.php'], [ + [ + 'Constant UnusedPrivateConstantEnum\Foo::TEST_2 is unused.', + 9, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + + public function testBug6758(): void + { + $this->analyse([__DIR__ . '/data/bug-6758.php'], []); + } + + 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 new file mode 100644 index 0000000000..ca57318a59 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -0,0 +1,146 @@ + + */ +class UnusedPrivateMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + 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 + { + $this->analyse([__DIR__ . '/data/unused-private-method.php'], [ + [ + 'Method UnusedPrivateMethod\Foo::doFoo() is unused.', + 8, + ], + [ + 'Method UnusedPrivateMethod\Foo::doBar() is unused.', + 13, + ], + [ + 'Static method UnusedPrivateMethod\Foo::unusedStaticMethod() is unused.', + 44, + ], + [ + 'Method UnusedPrivateMethod\Bar::doBaz() is unused.', + 59, + ], + [ + 'Method UnusedPrivateMethod\Lorem::doBaz() is unused.', + 99, + ], + [ + 'Method UnusedPrivateMethod\IgnoredByExtension::bar() is unused.', + 181, + ], + ]); + } + + public function testBug3630(): void + { + $this->analyse([__DIR__ . '/data/bug-3630.php'], []); + } + + public function testNullsafe(): void + { + $this->analyse([__DIR__ . '/data/nullsafe-unused-private-method.php'], []); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/callable-unused-private-method.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/unused-private-method-enum.php'], [ + [ + 'Method UnusedPrivateMethodEnunm\Foo::doBaz() is unused.', + 18, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..c83a84d425 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -0,0 +1,423 @@ + + */ +class UnusedPrivatePropertyRuleTest extends RuleTestCase +{ + + /** @var string[] */ + private array $alwaysWrittenTags; + + /** @var string[] */ + private array $alwaysReadTags; + + private bool $checkUninitializedProperties = false; + + protected function getRule(): Rule + { + return new UnusedPrivatePropertyRule( + new DirectReadWritePropertiesExtensionProvider([ + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'UnusedPrivateProperty\\TestExtension' + && in_array($propertyName, [ + 'read', + 'used', + ], true); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'UnusedPrivateProperty\\TestExtension' + && in_array($propertyName, [ + 'written', + 'used', + ], true); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + }, + ]), + $this->alwaysWrittenTags, + $this->alwaysReadTags, + $this->checkUninitializedProperties, + ); + } + + public function testRule(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->checkUninitializedProperties = true; + + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + + $this->analyse([__DIR__ . '/data/unused-private-property.php'], [ + [ + 'Property UnusedPrivateProperty\Foo::$bar is never read, only written.', + 10, + $tip, + ], + [ + 'Property UnusedPrivateProperty\Foo::$baz is unused.', + 12, + $tip, + ], + [ + 'Property UnusedPrivateProperty\Foo::$lorem is never written, only read.', + 14, + $tip, + ], + [ + 'Property UnusedPrivateProperty\Bar::$baz is never written, only read.', + 57, + $tip, + ], + [ + 'Static property UnusedPrivateProperty\Baz::$bar is never read, only written.', + 86, + $tip, + ], + [ + 'Static property UnusedPrivateProperty\Baz::$baz is unused.', + 88, + $tip, + ], + [ + 'Static property UnusedPrivateProperty\Baz::$lorem is never written, only read.', + 90, + $tip, + ], + [ + 'Property UnusedPrivateProperty\Lorem::$baz is never read, only written.', + 117, + $tip, + ], + [ + 'Property class@anonymous/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php:152::$bar is unused.', + 153, + $tip, + ], + [ + 'Property UnusedPrivateProperty\DolorWithAnonymous::$foo is unused.', + 148, + $tip, + ], + [ + 'Property UnusedPrivateProperty\ArrayAssign::$foo is never read, only written.', + 162, + $tip, + ], + [ + 'Property UnusedPrivateProperty\ListAssign::$foo is never read, only written.', + 191, + $tip, + ], + [ + 'Property UnusedPrivateProperty\WriteToCollection::$collection1 is never read, only written.', + 221, + $tip, + ], + [ + 'Property UnusedPrivateProperty\WriteToCollection::$collection2 is never read, only written.', + 224, + $tip, + ], + ]); + $this->analyse([__DIR__ . '/data/TestExtension.php'], [ + [ + 'Property UnusedPrivateProperty\TestExtension::$unused is unused.', + 8, + $tip, + ], + [ + 'Property UnusedPrivateProperty\TestExtension::$read is never written, only read.', + 10, + $tip, + ], + [ + 'Property UnusedPrivateProperty\TestExtension::$written is never read, only written.', + 12, + $tip, + ], + ]); + } + + public function testAlwaysUsedTags(): void + { + $this->alwaysWrittenTags = ['@ORM\Column']; + $this->alwaysReadTags = ['@get']; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + $this->analyse([__DIR__ . '/data/private-property-with-tags.php'], [ + [ + 'Property PrivatePropertyWithTags\Foo::$title is never read, only written.', + 13, + $tip, + ], + [ + 'Property PrivatePropertyWithTags\Foo::$text is never written, only read.', + 18, + $tip, + ], + ]); + } + + public function testTrait(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/private-property-trait.php'], []); + } + + public function testBug3636(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + $this->analyse([__DIR__ . '/data/bug-3636.php'], [ + [ + 'Property Bug3636\Bar::$date is never written, only read.', + 22, + $tip, + ], + ]); + } + + public function testPromotedProperties(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = ['@get']; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + $this->analyse([__DIR__ . '/data/unused-private-promoted-property.php'], [ + [ + 'Property UnusedPrivatePromotedProperty\Foo::$lorem is never read, only written.', + 12, + $tip, + ], + ]); + } + + public function testNullsafe(): void + { + $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 = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-5935.php'], []); + } + + public function testBug5337(): void + { + $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/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'], [ + [ + 'Readable property Bug12702\Foo2::$i is never read.', + 43, + ], + [ + 'Writable property Bug12702\Bar2::$i is never written.', + 54, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/TestExtension.php b/tests/PHPStan/Rules/DeadCode/data/TestExtension.php new file mode 100644 index 0000000000..5c2f9c8306 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/TestExtension.php @@ -0,0 +1,16 @@ += 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-2913.php b/tests/PHPStan/Rules/DeadCode/data/bug-2913.php new file mode 100644 index 0000000000..de3055094f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-2913.php @@ -0,0 +1,19 @@ +date ??= new \DateTimeImmutable(); + } + +} + +class Bar +{ + + /** @var \DateTimeImmutable */ + private $date; + + public function getDate(): ?\DateTimeImmutable + { + return $this->date ?? null; + } + +} + +class Baz +{ + + /** @var string */ + private $date; + + public function getDate(): string + { + return $this->date ?? ($this->date = random_bytes(16)); + } + +} + +class Lorem +{ + + /** @var string */ + private static $date; + + public function getDate(): string + { + return self::$date ?? (self::$date = random_bytes(16)); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-3654.php b/tests/PHPStan/Rules/DeadCode/data/bug-3654.php new file mode 100644 index 0000000000..d6e182a023 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-3654.php @@ -0,0 +1,42 @@ +id = $id; + } + + public function jsonSerialize(): array + { + return \get_object_vars($this); + } +} + +class Bar implements \JsonSerializable +{ + + /** + * @var int + */ + private $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function jsonSerialize(): void + { + \array_walk($this, static function ($key, $value) { + }); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php new file mode 100644 index 0000000000..f16716af43 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php @@ -0,0 +1,11 @@ +lex(); + + // recursively ignore nested objects + if ($code !== self::JSON_OBJECT_START) { + continue; + } + + $this->ignoreObjectBlock($lexer); + } while ($code !== self::JSON_OBJECT_END); + + return $lexer->lex(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-4535.php b/tests/PHPStan/Rules/DeadCode/data/bug-4535.php new file mode 100644 index 0000000000..c0feb30378 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-4535.php @@ -0,0 +1,16 @@ +prefix)) { + $this->prefix = $prefix; + } + } +} + +class Foo +{ + + private string $field; + + public function __construct() + { + if (isset($this->field)) {} + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-5651.php b/tests/PHPStan/Rules/DeadCode/data/bug-5651.php new file mode 100644 index 0000000000..88068fd018 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-5651.php @@ -0,0 +1,30 @@ +values = $values; + } +} + +class HelloWorld +{ + private const BAR = 'bar'; + + #[MyAttribute(['foo' => self::BAR])] + public function sayHello(): void + { + + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-5935.php b/tests/PHPStan/Rules/DeadCode/data/bug-5935.php new file mode 100644 index 0000000000..7a842a8fb7 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-5935.php @@ -0,0 +1,45 @@ +arr; + if(isset($arr[$id])) + { + return $arr[$id]; + } + else + { + return $arr[$id] = time(); + } + } +} + +class Test2 +{ + /** + * @var int[] + */ + private $arr; + + public function test(int $id): int + { + $arr = &$this->arr; + if(isset($arr[$id])) + { + return $arr[$id]; + } + else + { + return $arr[$id] = time(); + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-5971.php b/tests/PHPStan/Rules/DeadCode/data/bug-5971.php new file mode 100644 index 0000000000..816d892982 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-5971.php @@ -0,0 +1,33 @@ +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-6758.php b/tests/PHPStan/Rules/DeadCode/data/bug-6758.php new file mode 100644 index 0000000000..690baf82ca --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-6758.php @@ -0,0 +1,27 @@ +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/callable-unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/callable-unused-private-method.php new file mode 100644 index 0000000000..28b2b4f397 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/callable-unused-private-method.php @@ -0,0 +1,33 @@ += 8.1 + +namespace CallableUnusedPrivateMethod; + +class Foo +{ + + public function doFoo(): void + { + $f = $this->doBar(...); + } + + private function doBar(): void + { + + } + +} + +class Bar +{ + + public function doFoo(): void + { + $f = self::doBar(...); + } + + private static function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/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/nullsafe-property-fetch-noop.php b/tests/PHPStan/Rules/DeadCode/data/nullsafe-property-fetch-noop.php new file mode 100644 index 0000000000..4b32c173d8 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/nullsafe-property-fetch-noop.php @@ -0,0 +1,13 @@ += 8.0 + +namespace NullsafePropertyFetchNoop; + +class Foo +{ + + public function doFoo(?\ReflectionClass $ref): void + { + $ref?->name; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/nullsafe-unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/nullsafe-unused-private-method.php new file mode 100644 index 0000000000..59db9903be --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/nullsafe-unused-private-method.php @@ -0,0 +1,18 @@ += 8.0 + +namespace NullsafeUnusedPrivateMethod; + +class Foo +{ + + public function doFoo(?self $self): void + { + $self?->doBar(); + } + + private function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/nullsafe-unused-private-property.php b/tests/PHPStan/Rules/DeadCode/data/nullsafe-unused-private-property.php new file mode 100644 index 0000000000..a4e68bd586 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/nullsafe-unused-private-property.php @@ -0,0 +1,15 @@ += 8.0 + +namespace NullsafeUnusedPrivateProperty; + +class Foo +{ + + private string $bar = 'foo'; + + public function doFoo(?self $self): void + { + echo $self?->bar; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/private-property-trait.php b/tests/PHPStan/Rules/DeadCode/data/private-property-trait.php new file mode 100644 index 0000000000..753b43f778 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/private-property-trait.php @@ -0,0 +1,41 @@ +prop1; + } + + public function setFoo($prop1) + { + $this->prop1 = $prop1; + } + + public function getProp3() + { + return $this->prop3; + } + +} + +class ClassUsingTrait +{ + + use FooTrait; + + private $prop3; + + public function __construct(string $prop3) + { + $this->prop3 = $prop3; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/private-property-with-tags.php b/tests/PHPStan/Rules/DeadCode/data/private-property-with-tags.php new file mode 100644 index 0000000000..91c5beeb1f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/private-property-with-tags.php @@ -0,0 +1,26 @@ += 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-constant-enum.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-enum.php new file mode 100644 index 0000000000..63daa4fb44 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-enum.php @@ -0,0 +1,16 @@ += 8.1 + +namespace UnusedPrivateConstantEnum; + +enum Foo +{ + + private const TEST = 1; + private const TEST_2 = 1; + + public function doFoo(): void + { + echo self::TEST; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php new file mode 100644 index 0000000000..88531598b2 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php @@ -0,0 +1,46 @@ +ignoreObjectBlock(); + } while ($code !== self::JSON_OBJECT_END); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-method-enum.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-method-enum.php new file mode 100644 index 0000000000..d3ab7e71eb --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method-enum.php @@ -0,0 +1,23 @@ += 8.1 + +namespace UnusedPrivateMethodEnunm; + +enum Foo +{ + + public function doFoo(): void + { + $this->doBar(); + } + + private function doBar(): void + { + + } + + private function doBaz(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php new file mode 100644 index 0000000000..ce43e40a90 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php @@ -0,0 +1,184 @@ +doFoo(); + } + + private function doBar() + { + $this->doBaz(); + } + + private function doBaz() + { + self::calledStatically(); + } + + private function calledStatically() + { + + } + + private function __construct() + { + $this->staticMethod(); + self::anotherStaticMethod(); + } + + private static function staticMethod() + { + + } + + private static function anotherStaticMethod() + { + + } + + private static function unusedStaticMethod() + { + + } + +} + +class Bar +{ + + private function doFoo() + { + + } + + private function doBaz() + { + $cb = [$this, 'doBaz']; + $cb(); + } + + public function doBar() + { + $cb = [$this, 'doFoo']; + $cb(); + } + +} + +class Baz +{ + + private function doFoo() + { + + } + + public function doBar(string $name) + { + if ($name === 'doFoo') { + $cb = [$this, $name]; + $cb(); + } + } + +} + +class Lorem +{ + + private function doFoo() + { + + } + + private function doBaz() + { + + } + + public function doBar() + { + $m = 'doFoo'; + $this->{$m}(); + } + +} + +class Ipsum +{ + + private function doFoo() + { + + } + + public function doBar(string $s) + { + $this->{$s}(); + } + +} + +trait FooTrait +{ + + private function doFoo() + { + + } + + private function doBar() + { + + } + + public function doBaz() + { + $this->doFoo(); + $this->doLorem(); + } + +} + +class UsingFooTrait +{ + + use FooTrait; + + 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-promoted-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-promoted-property.php new file mode 100644 index 0000000000..81c6e3934c --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-promoted-property.php @@ -0,0 +1,21 @@ += 8.0 + +namespace UnusedPrivatePromotedProperty; + +class Foo +{ + + public function __construct( + public $foo, + protected $bar, + private $baz, + private $lorem, + /** @get */ private $ipsum + ) { } + + public function getBaz() + { + return $this->baz; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php new file mode 100644 index 0000000000..97aa923a37 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php @@ -0,0 +1,232 @@ +foo = 1; + $this->bar = 2; + $this->ipsum['foo']['bar'] = 3; + $this->dolor++; + } + + public function getFoo() + { + return $this->foo; + } + + public function getLorem() + { + return $this->lorem; + } + + public function getIpsum() + { + return $this->ipsum; + } + + public function getDolor(): int + { + return $this->dolor; + } + +} + +class Bar +{ + + private int $foo; + + private int $bar; // do not report read-only, it's uninitialized + + private $baz; // report read-only + + public function __construct() + { + $this->foo = 1; + } + + public function getFoo(): int + { + return $this->foo; + } + + public function getBar(): int + { + return $this->bar; + } + + public function getBaz(): int + { + return $this->baz; + } + +} + +class Baz +{ + + private static $foo; + + private static $bar; // write-only + + private static $baz; // unused + + private static $lorem; // read-only + + public static function doFoo() + { + self::$foo = 1; + self::$bar = 2; + } + + public static function getFoo() + { + return self::$foo; + } + + public static function getLorem() + { + return self::$lorem; + } + +} + +class Lorem +{ + + private $foo = 'foo'; + + private $bar = 'bar'; + + private $baz = 'baz'; + + public function doFoo() + { + $nameProperties = [ + 'foo', + 'bar', + ]; + + foreach ($nameProperties as $nameProperty) { + echo "Hello, {$this->$nameProperty}"; + } + } + +} + +class Ipsum +{ + + private $foo = 'foo'; + + public function doBar(string $s) + { + echo $this->{$s}; + } + +} + +class DolorWithAnonymous +{ + + private $foo; + + public function doFoo() + { + new class () { + private $bar; + }; + } + +} + +class ArrayAssign +{ + + private $foo; + + public function doFoo(): void + { + [$this->foo] = [1]; + } + +} + +class ArrayAssignAndRead +{ + + private $foo; + + public function doFoo(): void + { + [$this->foo] = [1]; + } + + public function getFoo() + { + return $this->foo; + } + +} + +class ListAssign +{ + + private $foo; + + public function doFoo(): void + { + list($this->foo) = [1]; + } + +} + +class ListAssignAndRead +{ + + private $foo; + + public function doFoo(): void + { + list($this->foo) = [1]; + } + + public function getFoo() + { + return $this->foo; + } + +} + +class WriteToCollection +{ + + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } + +} diff --git a/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php b/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php new file mode 100644 index 0000000000..ff5294eded --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php @@ -0,0 +1,56 @@ + + */ +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 new file mode 100644 index 0000000000..7a2ff3aa6a --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -0,0 +1,105 @@ + + */ +class DumpTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DumpTypeRule($this->createReflectionProvider()); + } + + public function testRuleInPhpStanNamespace(): void + { + $this->analyse([__DIR__ . '/data/dump-type.php'], [ + [ + 'Dumped type: non-empty-array', + 10, + ], + ]); + } + + public function testRuleInDifferentNamespace(): void + { + $this->analyse([__DIR__ . '/data/dump-type-ns.php'], [ + [ + 'Dumped type: non-empty-array', + 10, + ], + ]); + } + + public function testRuleInUse(): void + { + $this->analyse([__DIR__ . '/data/dump-type-use.php'], [ + [ + 'Dumped type: non-empty-array', + 12, + ], + [ + 'Dumped type: non-empty-array', + 13, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..aec3ed1500 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/FileAssertRuleTest.php @@ -0,0 +1,49 @@ + + */ +class FileAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FileAssertRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/file-asserts.php'], [ + [ + 'Expected type array, actual: array', + 19, + ], + [ + 'Expected native type false, actual: bool', + 36, + ], + [ + 'Expected native type true, actual: bool', + 37, + ], + [ + 'Expected variable $b certainty Yes, actual: No', + 45, + ], + [ + '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/dump-type-ns.php b/tests/PHPStan/Rules/Debug/data/dump-type-ns.php new file mode 100644 index 0000000000..b031002326 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/dump-type-ns.php @@ -0,0 +1,12 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + assertType('array', $a); + } + + /** + * @param non-empty-array $a + */ + public function doBar(array $a): void + { + assertType('non-empty-array', $a); + assertNativeType('array', $a); + + assertType('false', $a === []); + assertType('true', $a !== []); + + assertNativeType('bool', $a === []); + assertNativeType('bool', $a !== []); + + assertNativeType('false', $a === []); + assertNativeType('true', $a !== []); + } + + public function doBaz($a): void + { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertVariableCertainty(TrinaryLogic::createNo(), $b); + + assertVariableCertainty(TrinaryLogic::createYes(), $b); + 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/DummyRule.php b/tests/PHPStan/Rules/DummyRule.php index 697c9a4ead..02f6939690 100644 --- a/tests/PHPStan/Rules/DummyRule.php +++ b/tests/PHPStan/Rules/DummyRule.php @@ -6,9 +6,9 @@ use PHPStan\Analyser\Scope; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class DummyRule implements \PHPStan\Rules\Rule +class DummyRule implements Rule { public function getNodeType(): string diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php new file mode 100644 index 0000000000..5de7d45b5d --- /dev/null +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -0,0 +1,65 @@ + + */ +class EnumCaseAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new EnumCaseAttributesRule( + new AttributesCheck( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper($reflectionProvider, true, false, true, false, 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 < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-case-attributes.php'], [ + [ + 'Attribute class EnumCaseAttributes\AttributeWithPropertyTarget does not have the class constant target.', + 26, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/EnumCases/data/enum-case-attributes.php b/tests/PHPStan/Rules/EnumCases/data/enum-case-attributes.php new file mode 100644 index 0000000000..74c4f6c4ff --- /dev/null +++ b/tests/PHPStan/Rules/EnumCases/data/enum-case-attributes.php @@ -0,0 +1,45 @@ += 8.1 + +namespace EnumCaseAttributes; + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AttributeWithPropertyTarget +{ + +} + +#[\Attribute(\Attribute::TARGET_CLASS_CONSTANT)] +class AttributeWithClassConstantTarget +{ + +} + +#[\Attribute(\Attribute::TARGET_ALL)] +class AttributeWithTargetAll +{ + +} + +enum Lorem +{ + + #[AttributeWithPropertyTarget] + case FOO; + +} + +enum Ipsum +{ + + #[AttributeWithClassConstantTarget] + case FOO; + +} + +enum Dolor +{ + + #[AttributeWithTargetAll] + case FOO; + +} diff --git a/tests/PHPStan/Rules/Exceptions/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/Bug5364Test.php b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php new file mode 100644 index 0000000000..e6c4a38a42 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php @@ -0,0 +1,39 @@ + + */ +class Bug5364Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInMethodThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + )), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-5364.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-5364.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/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 new file mode 100644 index 0000000000..f9b8b96d7c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -0,0 +1,661 @@ + + */ +class CatchWithUnthrownExceptionRuleTest extends RuleTestCase +{ + + private bool $reportUncheckedExceptionDeadCatch = true; + + /** @var string[] */ + private array $uncheckedExceptionClasses = []; + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + $this->uncheckedExceptionClasses, + [], + [], + ), $this->reportUncheckedExceptionDeadCatch); + } + + public function testRule(): void + { + $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 - InvalidArgumentException is never thrown in the try block.', + 84, + ], + [ + '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 - ArithmeticError is never thrown in the try block.', + 260, + ], + [ + 'Dead catch - ArithmeticError is never thrown in the try block.', + 279, + ], + [ + '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, + ], + [ + '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, + ], + ]); + } + + public function testBug4806(): void + { + $this->analyse([__DIR__ . '/data/bug-4806.php'], [ + [ + 'Dead catch - ArgumentCountError is never thrown in the try block.', + 65, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 119, + ], + ]); + } + + public function testBug4805(): void + { + $this->analyse([__DIR__ . '/data/bug-4805.php'], [ + [ + 'Dead catch - OutOfBoundsException is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - OutOfBoundsException is never thrown in the try block.', + 66, + ], + ]); + } + + public function testBug4863(): void + { + $this->analyse([__DIR__ . '/data/bug-4863.php'], []); + } + + public function testBug5866(): void + { + 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.', + 16, + ], + ]); + } + + public function testBug9066(): void + { + $this->analyse([__DIR__ . '/data/bug-9066.php'], [ + [ + 'Dead catch - OutOfBoundsException is never thrown in the try block.', + 28, + ], + ]); + } + + public function testThrowExpression(): void + { + $this->analyse([__DIR__ . '/data/dead-catch-throw-expr.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 17, + ], + ]); + } + + public function testDeadCatch(): void + { + $this->analyse([__DIR__ . '/data/dead-catch.php'], [ + [ + 'Dead catch - TypeError is already caught above.', + 27, + ], + ]); + } + + public function testFirstClassCallables(): void + { + $this->analyse([__DIR__ . '/data/dead-catch-first-class-callables.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 29, + ], + ]); + } + + public function testBug4852(): void + { + $this->analyse([__DIR__ . '/data/bug-4852.php'], [ + [ + 'Dead catch - Exception is never thrown in the try block.', + 63, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 78, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 85, + ], + ]); + } + + public function testBug5903(): void + { + $this->analyse([__DIR__ . '/data/bug-5903.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 47, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 54, + ], + ]); + } + + public function 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'], []); + } + + public function testBug6256(): void + { + $this->analyse([__DIR__ . '/data/bug-6256.php'], [ + [ + 'Dead catch - TypeError is never thrown in the try block.', + 25, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 31, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 45, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 57, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 63, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 100, + ], + ]); + } + + public function testBug6791(): void + { + $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 ffb6c0bf33..1ec6854a48 100644 --- a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php @@ -3,20 +3,30 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CaughtExceptionExistenceRuleTest extends \PHPStan\Testing\RuleTestCase +class CaughtExceptionExistenceRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $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, ); } @@ -30,6 +40,8 @@ public function testCheckCaughtException(): void [ 'Caught class FooCatchException not found.', 29, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], [ 'Class TestCatch\MyCatchException referenced with incorrect case: TestCatch\MyCatchEXCEPTION.', @@ -43,4 +55,22 @@ public function testClassExists(): void $this->analyse([__DIR__ . '/data/class-exists.php'], []); } + 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/DeadCatchRuleTest.php b/tests/PHPStan/Rules/Exceptions/DeadCatchRuleTest.php deleted file mode 100644 index 828cab35ce..0000000000 --- a/tests/PHPStan/Rules/Exceptions/DeadCatchRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class DeadCatchRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new DeadCatchRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/dead-catch.php'], [ - [ - 'Dead catch - TypeError is already caught by Throwable above.', - 27, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php new file mode 100644 index 0000000000..0708028924 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php @@ -0,0 +1,149 @@ +createReflectionProvider(), $uncheckedExceptionRegexes, $uncheckedExceptionClasses, $checkedExceptionRegexes, $checkedExceptionClasses); + $this->assertSame($expectedResult, $resolver->isCheckedException($className, self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create(__DIR__)))); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php new file mode 100644 index 0000000000..716e7b8687 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php @@ -0,0 +1,55 @@ + + */ +class MissingCheckedExceptionInFunctionThrowsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInFunctionThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [ShouldNotHappenException::class], + [], + [], + )), + ); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/missing-exception-function-throws.php'; + $this->analyse([__DIR__ . '/data/missing-exception-function-throws.php'], [ + [ + 'Function MissingExceptionFunctionThrows\doBaz() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 20, + ], + [ + 'Function MissingExceptionFunctionThrows\doLorem() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 26, + ], + [ + 'Function MissingExceptionFunctionThrows\doLorem2() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 31, + ], + [ + 'Function MissingExceptionFunctionThrows\doBar2() throws checked exception LogicException but it\'s missing from the PHPDoc @throws tag.', + 51, + ], + [ + 'Function MissingExceptionFunctionThrows\doBar3() throws checked exception LogicException but it\'s missing from the PHPDoc @throws tag.', + 57, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php new file mode 100644 index 0000000000..ba61e2900a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -0,0 +1,73 @@ + + */ +class MissingCheckedExceptionInMethodThrowsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInMethodThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [ShouldNotHappenException::class], + [], + [], + )), + ); + } + + public function testRule(): void + { + $errors = [ + [ + 'Method MissingExceptionMethodThrows\Foo::doBaz() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 23, + ], + [ + 'Method MissingExceptionMethodThrows\Foo::doLorem() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', + 29, + ], + [ + '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/OverwrittenExitPointByFinallyRuleTest.php b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php new file mode 100644 index 0000000000..1698ada07c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php @@ -0,0 +1,179 @@ + + */ +class OverwrittenExitPointByFinallyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OverwrittenExitPointByFinallyRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/overwritten-exit-point.php'], [ + [ + 'This throw is overwritten by a different one in the finally block below.', + 8, + ], + [ + 'This return is overwritten by a different one in the finally block below.', + 11, + ], + [ + 'This return is overwritten by a different one in the finally block below.', + 13, + ], + [ + 'The overwriting return is on this line.', + 15, + ], + ]); + } + + public function testBug5627(): void + { + $this->analyse([__DIR__ . '/data/bug-5627.php'], [ + [ + 'This throw is overwritten by a different one in the finally block below.', + 10, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 12, + ], + [ + 'The overwriting return is on this line.', + 14, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 29, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 31, + ], + [ + 'The overwriting return is on this line.', + 33, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 39, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 41, + ], + [ + 'The overwriting return is on this line.', + 43, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 49, + ], + [ + 'The overwriting return is on this line.', + 51, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 62, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 64, + ], + [ + 'The overwriting return is on this line.', + 66, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 81, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 83, + ], + [ + 'The overwriting return is on this line.', + 85, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 91, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 93, + ], + [ + 'The overwriting return is on this line.', + 95, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 101, + ], + [ + 'The overwriting return is on this line.', + 103, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 122, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 124, + ], + [ + 'The overwriting return is on this line.', + 126, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 141, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 143, + ], + [ + 'The overwriting return is on this line.', + 145, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 151, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 153, + ], + [ + 'The overwriting return is on this line.', + 155, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 161, + ], + [ + 'The overwriting return is on this line.', + 163, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/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 new file mode 100644 index 0000000000..1f32de8fe4 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php @@ -0,0 +1,51 @@ + + */ +class ThrowExpressionRuleTest extends RuleTestCase +{ + + private PhpVersion $phpVersion; + + protected function getRule(): Rule + { + return new ThrowExpressionRule($this->phpVersion); + } + + public function dataRule(): array + { + return [ + [ + 70400, + [ + [ + 'Throw expression is supported only on PHP 8.0 and later.', + 10, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param list $expectedErrors + */ + public function testRule(int $phpVersion, array $expectedErrors): void + { + $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 new file mode 100644 index 0000000000..ff6c4416a6 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -0,0 +1,99 @@ + + */ +class ThrowsVoidFunctionWithExplicitThrowPointRuleTest extends RuleTestCase +{ + + private bool $missingCheckedExceptionInThrows; + + /** @var string[] */ + private array $checkedExceptionClasses; + + protected function getRule(): Rule + { + return new ThrowsVoidFunctionWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + $this->checkedExceptionClasses, + ), $this->missingCheckedExceptionInThrows); + } + + public function dataRule(): array + { + return [ + [ + true, + [], + [], + ], + [ + false, + ['DifferentException'], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + [ + true, + [MyException::class], + [], + ], + [ + true, + ['DifferentException'], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + [ + false, + [], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + [ + false, + [MyException::class], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param string[] $checkedExceptionClasses + * @param list $errors + */ + public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void + { + $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; + $this->checkedExceptionClasses = $checkedExceptionClasses; + $this->analyse([__DIR__ . '/data/throws-void-function.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php new file mode 100644 index 0000000000..5a2dcb0429 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -0,0 +1,111 @@ + + */ +class ThrowsVoidMethodWithExplicitThrowPointRuleTest extends RuleTestCase +{ + + private bool $missingCheckedExceptionInThrows; + + /** @var string[] */ + private array $checkedExceptionClasses; + + protected function getRule(): Rule + { + return new ThrowsVoidMethodWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + $this->checkedExceptionClasses, + ), $this->missingCheckedExceptionInThrows); + } + + public function dataRule(): array + { + return [ + [ + true, + [], + [], + ], + [ + false, + ['DifferentException'], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + [ + true, + [MyException::class], + [], + ], + [ + true, + ['DifferentException'], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + [ + false, + [], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + [ + false, + [MyException::class], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param string[] $checkedExceptionClasses + * @param list $errors + */ + public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void + { + $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; + $this->checkedExceptionClasses = $checkedExceptionClasses; + $this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors); + } + + 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 new file mode 100644 index 0000000000..8de4ae7bed --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php @@ -0,0 +1,47 @@ + + */ +class TooWideFunctionThrowTypeRuleTest extends RuleTestCase +{ + + private bool $implicitThrows = true; + + protected function getRule(): Rule + { + 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, + ], + [ + 'Function TooWideThrowsFunction\doFoo7() has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 48, + ], + [ + 'Function TooWideThrowsFunction\doFoo8() has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 57, + ], + [ + 'Function TooWideThrowsFunction\doFoo9() has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 63, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php new file mode 100644 index 0000000000..c3f2887c98 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -0,0 +1,108 @@ + + */ +class TooWideMethodThrowTypeRuleTest extends RuleTestCase +{ + + private bool $implicitThrows = true; + + protected function getRule(): Rule + { + 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, + ], + [ + 'Method TooWideThrowsMethod\Foo::doFoo7() has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 51, + ], + [ + 'Method TooWideThrowsMethod\Foo::doFoo8() has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 60, + ], + [ + 'Method TooWideThrowsMethod\Foo::doFoo9() has DomainException in PHPDoc @throws tag but it\'s not thrown.', + 66, + ], + [ + '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/bug-5364.neon b/tests/PHPStan/Rules/Exceptions/bug-5364.neon new file mode 100644 index 0000000000..93a3c1512a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/bug-5364.neon @@ -0,0 +1,6 @@ +parameters: + exceptions: + implicitThrows: false + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true diff --git a/tests/PHPStan/Rules/Exceptions/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-3690.php b/tests/PHPStan/Rules/Exceptions/data/bug-3690.php new file mode 100644 index 0000000000..b2ce8e6b96 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-3690.php @@ -0,0 +1,18 @@ + $foo */ + public static function doBar(string $foo, bool $flag): bool + { + try{ + $foo::canThrow($flag); + return true; + } catch(\OutOfBoundsException $e){ + return false; + } + } + + /** @param class-string $foo */ + public static function doBar2(string $foo, bool $flag): bool + { + try{ + $foo::cannotThrow($flag); + return true; + } catch(\OutOfBoundsException $e){ + return false; + } + } + + /** @param class-string $foo */ + public static function doBaz(string $foo, bool $flag): bool + { + try{ + $foo::canThrow($flag); + return true; + } catch(\OutOfBoundsException $e){ + return false; + } + } + + /** @param class-string $foo */ + public static function doBaz2(string $foo, bool $flag): bool + { + try{ + $foo::cannotThrow($flag); + return true; + } catch(\OutOfBoundsException $e){ + return false; + } + } + + public static function doLorem(string $foo, bool $flag): bool + { + try{ + $foo::canThrow($flag); + return true; + } catch(\OutOfBoundsException $e){ + return false; + } + } + + public static function doLorem2(string $foo, bool $flag): bool + { + try{ + $foo::cannotThrow($flag); + return true; + } catch(\OutOfBoundsException $e){ + return false; + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-4806.php b/tests/PHPStan/Rules/Exceptions/data/bug-4806.php new file mode 100644 index 0000000000..78d1ae5d38 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4806.php @@ -0,0 +1,124 @@ + $class + */ + function createNeverThrows(string $class): object + { + try { + $object = new $class(); + } catch (\ArgumentCountError $throwable) { + + } + + return $object; + } + + /** + * @param class-string $class + */ + function createMayThrowArgumentCountError(string $class): object + { + try { + $object = new $class(); + } catch (\ArgumentCountError $error) { + + } + + return $object; + } + + /** + * @param class-string $class + */ + function createMayThrowArgumentCountErrorB(string $class): object + { + try { + $object = new $class(); + } catch (\Throwable $throwable) { + + } + + return $object; + } + + /** + * @param class-string $class + */ + function implicitThrow(string $class): void + { + try { + $object = new $class(); + } catch (\Throwable $throwable) { + + } + } + + /** + * @param class-string $class + */ + function hasNoConstructor(string $class): void + { + try { + $object = new $class(); + } catch (\Throwable $throwable) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-4814.php b/tests/PHPStan/Rules/Exceptions/data/bug-4814.php new file mode 100644 index 0000000000..9959138f6e --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4814.php @@ -0,0 +1,19 @@ + + */ + public function generate(): \Generator { + try { + yield 'test'; + } catch (\Exception $exception) { + echo $exception->getMessage(); + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5364.php b/tests/PHPStan/Rules/Exceptions/data/bug-5364.php new file mode 100644 index 0000000000..b52c6a1d36 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5364.php @@ -0,0 +1,20 @@ +test(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5627.php b/tests/PHPStan/Rules/Exceptions/data/bug-5627.php new file mode 100644 index 0000000000..570d8aabdc --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5627.php @@ -0,0 +1,167 @@ +abort(); + } catch (\Exception $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function c(): string { + try { + $this->abort(); + } catch (\Throwable $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function d(): string { + try { + $this->abort(); + } finally { + return 'finally'; + } + } + +} + +class Bar +{ + + public function a(): string { + try { + throw new \Exception('try'); + } catch (\Exception $e) { + throw new \Exception('catch'); + } finally { + return 'finally'; + } + } + + /** + * + * @return never + */ + public function abort() + { + throw new \Exception(); + } + + public function b(): string { + try { + $this->abort(); + } catch (\Exception $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function c(): string { + try { + $this->abort(); + } catch (\Throwable $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function d(): string { + try { + $this->abort(); + } finally { + return 'finally'; + } + } + +} + +/** + * @return never + */ +function abort() +{ + +} + +class Baz +{ + + public function a(): string { + try { + throw new \Exception('try'); + } catch (\Exception $e) { + throw new \Exception('catch'); + } finally { + return 'finally'; + } + } + + + + + + + + + + + public function b(): string { + try { + abort(); + } catch (\Exception $e) { + abort(); + } finally { + return 'finally'; + } + } + + public function c(): string { + try { + abort(); + } catch (\Throwable $e) { + abort(); + } finally { + return 'finally'; + } + } + + public function d(): string { + try { + abort(); + } finally { + return 'finally'; + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-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 new file mode 100644 index 0000000000..0300b6ecc8 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php @@ -0,0 +1,64 @@ + */ + protected $traversable; + /** @var \Iterator */ + protected $iterator; + /** @var iterable */ + protected $iterable; + /** @var array */ + protected $array; + /** @var array|null */ + protected $maybeArray; + /** @var \Iterator|null */ + protected $maybeIterable; + + public function foo() + { + try { + foreach ($this->traversable as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->iterator as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->iterable as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->array as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->maybeArray as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->maybeIterable as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-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 @@ +integerType = "string"; + } catch (\TypeError $e) { + // not dead + } + + try { + $this->mixedType = "string"; + } catch (\TypeError $e) { + // dead + } + + try { + $this->stringType = "string"; + } catch (\TypeError $e) { + // dead + } + + /** @var string|int $intOrString */ + $intOrString = ''; + try { + $this->integerType = $intOrString; + } catch (\TypeError $e) { + // not dead + } + + try { + $this->stringOrIntType = 1; + } catch (\TypeError $e) { + // dead + } + + try { + $this->integerType = "string"; + } catch (\Error $e) { + // not dead + } + + try { + $this->integerType = "string"; + } catch (\Exception $e) { + // dead + } + + try { + $this->dynamicProperty = 1; + } catch (\Throwable $e) { + // dead + } + } +} + +final class B { + + /** + * @throws Exception + */ + public function __set(string $name, $value) + { + throw new Exception(); + } + + function doFoo() + { + try { + $this->dynamicProperty = "string"; + } catch (\Exception $e) { + // not dead + } + } +} + +final class C { + + /** + * @throws void + */ + public function __set(string $name, $value) {} + + function doFoo() + { + try { + $this->dynamicProperty = "string"; + } catch (\Exception $e) { + // dead + } + } +} + +class D { + function doFoo() + { + try { + $this->dynamicProperty = "string"; + } catch (\Exception $e) { + // not dead because class is not final + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6262.php b/tests/PHPStan/Rules/Exceptions/data/bug-6262.php new file mode 100644 index 0000000000..d11b718d17 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6262.php @@ -0,0 +1,20 @@ +|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-first-class-callables.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch-first-class-callables.php new file mode 100644 index 0000000000..a212159e99 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch-first-class-callables.php @@ -0,0 +1,34 @@ += 8.1 + +namespace DeadCatchFirstClassCallables; + +class Foo +{ + + public function doFoo(): void + { + try { + $this->doBar(); + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws \InvalidArgumentException + */ + public function doBar(): void + { + throw new \InvalidArgumentException(); + } + + public function doBaz(): void + { + try { + $this->doBar(...); + } catch (\InvalidArgumentException $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch-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/dead-catch-throw-expr.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch-throw-expr.php new file mode 100644 index 0000000000..0fb0a83a29 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch-throw-expr.php @@ -0,0 +1,20 @@ += 8.0 + +namespace DeadCatchThrowExpression; + +function (): void { + try { + $foo = throw new \InvalidArgumentException('foo'); + } catch (\InvalidArgumentException $e) { + + } +}; + +function (): void { + try { + /** @throws void */ + $foo = throw new \InvalidArgumentException('foo'); + } catch (\InvalidArgumentException $e) { + + } +}; diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch.php index a2765c41d9..cb583d37c3 100644 --- a/tests/PHPStan/Rules/Exceptions/data/dead-catch.php +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch.php @@ -8,7 +8,7 @@ class Foo public function doFoo() { try { - + doFoo(); } catch (\Exception $e) { } catch (\TypeError $e) { @@ -21,7 +21,7 @@ public function doFoo() public function doBar() { try { - + doFoo(); } catch (\Throwable $e) { } catch (\TypeError $e) { @@ -29,4 +29,11 @@ public function doBar() } } + function evalString(string $evalString) { + try { + eval($evalString); + } catch (\Throwable $exception) { + // + } + } } diff --git a/tests/PHPStan/Rules/Exceptions/data/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-function-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php new file mode 100644 index 0000000000..fa699f6bd7 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php @@ -0,0 +1,58 @@ +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/Exceptions/data/overwritten-exit-point.php b/tests/PHPStan/Rules/Exceptions/data/overwritten-exit-point.php new file mode 100644 index 0000000000..05b29bb86e --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/overwritten-exit-point.php @@ -0,0 +1,33 @@ += 8.0 + +namespace ThrowExpr; + +class Bar +{ + + public function doFoo(bool $b): void + { + $b ? true : throw new \Exception(); + + throw new \Exception(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/throw-values-nullsafe.php b/tests/PHPStan/Rules/Exceptions/data/throw-values-nullsafe.php new file mode 100644 index 0000000000..91dd2b516f --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throw-values-nullsafe.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ThrowExprValuesNullsafe; + +class Bar +{ + + function doException(): \Exception + { + return new \Exception(); + } + +} + +function doFoo(?Bar $bar) +{ + throw $bar?->doException(); +} diff --git a/tests/PHPStan/Rules/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-function.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php new file mode 100644 index 0000000000..d5a407ec13 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php @@ -0,0 +1,16 @@ += 8.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 new file mode 100644 index 0000000000..d537543139 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-function.php @@ -0,0 +1,72 @@ += 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 new file mode 100644 index 0000000000..0327fcb086 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -0,0 +1,792 @@ +throwIae(); + } catch (\InvalidArgumentException $e) { + + } catch (\Exception $e) { + // dead + } catch (\Throwable $e) { + // not dead + } + } + + public function doLorem(): void + { + try { + $this->throwIae(); + } catch (\RuntimeException $e) { + // dead + } catch (\Throwable $e) { + + } + } + + public function doIpsum(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } + } + + public function doDolor(): void + { + try { + throw new \InvalidArgumentException(); + } catch (\InvalidArgumentException $e) { + + } catch (\Throwable $e) { + + } + } + + public function doSit(): void + { + try { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\InvalidArgumentException $e) { + + } + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws \InvalidArgumentException + * @throws \DomainException + */ + public function doAmet() + { + + } + + public function doAmet1() + { + try { + $this->doAmet(); + } catch (\InvalidArgumentException $e) { + + } catch (\DomainException $e) { + + } catch (\Throwable $e) { + // not dead + } + } + + public function doAmet2() + { + try { + throw new \InvalidArgumentException(); + } catch (\InvalidArgumentException $e) { + + } catch (\DomainException $e) { + // dead + } catch (\Throwable $e) { + // dead + } + } + + public function doConsecteur() + { + try { + if (false) { + + } elseif ($this->doAmet()) { + + } + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class InlineThrows +{ + + public function doFoo() + { + try { + /** @throws \InvalidArgumentException */ + echo 1; + } catch (\InvalidArgumentException $e) { + + } + } + + public function doBar() + { + try { + /** @throws \InvalidArgumentException */ + $i = 1; + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class TestDateTime +{ + + public function doFoo(): void + { + try { + new \DateTime(); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \DateTime('now'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $s): void + { + try { + new \DateTime($s); + } catch (\Exception $e) { + + } + } + + /** + * @phpstan-param 'now'|class-string $s + */ + public function doSuperBaz(string $s): void + { + try { + new \DateTime($s); + } catch (\Exception $e) { + + } + } + +} + +class TestDateInterval +{ + + public function doFoo(): void + { + try { + new \DateInterval('invalid format'); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \DateInterval('P10D'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $s): void + { + try { + new \DateInterval($s); + } catch (\Exception $e) { + + } + } + + /** + * @phpstan-param 'P10D'|class-string $s + */ + public function doSuperBaz(string $s): void + { + try { + new \DateInterval($s); + } catch (\Exception $e) { + + } + } + +} + +class TestIntdiv +{ + + public function doFoo(): void + { + try { + intdiv(1, 1); + intdiv(1, -1); + } catch (\ArithmeticError $e) { + + } + try { + intdiv(PHP_INT_MIN, -1); + } catch (\ArithmeticError $e) { + + } + try { + intdiv(1, 0); + } catch (\ArithmeticError $e) { + + } + } + + public function doBar(int $int): void + { + try { + intdiv($int, 1); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, -1); + } catch (\ArithmeticError $e) { + + } + } + + public function doBaz(int $int): void + { + try { + intdiv(1, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv(PHP_INT_MIN, $int); + } catch (\ArithmeticError $e) { + + } + } + +} + +class TestSimpleXMLElement +{ + + 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) { + + } + } + +} + +class TestReflectionClass +{ + + public function doFoo(): void + { + try { + new \ReflectionClass(\DateTime::class); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \ReflectionClass('ThisIsNotARealClass'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \ReflectionClass($string); + } catch (\Exception $e) { + + } + } + + /** + * @param \DateTime|\DateTimeImmutable|class-string<\DateTime> $rightClassOrObject + * @param \DateTime|\DateTimeImmutable|string $wrongClassOrObject + */ + public function doThing(object $foo, $rightClassOrObject, $wrongClassOrObject): void + { + try { + new \ReflectionClass($foo); + } catch (\Exception $e) { + + } + try { + new \ReflectionClass($rightClassOrObject); + } catch (\Exception $e) { + + } + try { + new \ReflectionClass($wrongClassOrObject); + } catch (\Exception $e) { + + } + } +} + +class TestReflectionFunction +{ + + public function doFoo(): void + { + try { + new \ReflectionFunction('is_string'); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \ReflectionFunction('foo'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \ReflectionFunction($string); + } catch (\Exception $e) { + + } + } + +} + +class TestReflectionMethod +{ + /** + * @param class-string<\DateTimeInterface> $foo + */ + public function doFoo($foo): void + { + try { + new \ReflectionMethod(\DateTime::class, 'format'); + } catch (\Exception $e) { + + } + try { + new \ReflectionMethod($foo, 'format'); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \ReflectionMethod('foo', 'format'); + } catch (\Exception $e) { + + } + try { + new \ReflectionMethod(\DateTime::class, 'foo'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \ReflectionMethod($string, $string); + } catch (\Exception $e) { + + } + try { + new \ReflectionMethod(\DateTime::class, $string); + } catch (\Exception $e) { + + } + try { + new \ReflectionMethod($string, 'foo'); + } catch (\Exception $e) { + + } + } + +} + +class TestReflectionProperty +{ + public $foo; + + public function doFoo(): void + { + try { + new \ReflectionProperty(self::class, 'foo'); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \ReflectionProperty(self::class, 'bar'); + } catch (\Exception $e) { + + } + try { + new \ReflectionProperty(\DateTime::class, 'bar'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \ReflectionProperty($string, $string); + } catch (\Exception $e) { + + } + try { + new \ReflectionProperty(self::class, $string); + } catch (\Exception $e) { + + } + try { + new \ReflectionProperty($string, 'foo'); + } catch (\Exception $e) { + + } + } + +} + +class ExceptionGetMessage +{ + + public function doFoo(\Exception $e) + { + try { + echo $e->getMessage(); + } catch (\Exception $t) { + + } + } + + public function doBar(string $s) + { + try { + $this->{'doFoo' . $s}(); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +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 new file mode 100644 index 0000000000..a46163b89c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -0,0 +1,60 @@ + + */ +class ArrowFunctionAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new ArrowFunctionAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/arrow-function-attributes.php'], [ + [ + 'Attribute class ArrowFunctionAttributes\Foo does not have the function target.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php new file mode 100644 index 0000000000..d046acb657 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnNullsafeByRefRuleTest.php @@ -0,0 +1,30 @@ + + */ +class ArrowFunctionReturnNullsafeByRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ArrowFunctionReturnNullsafeByRefRule(new NullsafeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/arrow-function-nullsafe-by-ref.php'], [ + [ + 'Nullsafe cannot be returned by reference.', + 6, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index 84a33908fd..a5f2fa7232 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -6,9 +6,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ArrowFunctionReturnTypeRuleTest extends RuleTestCase { @@ -20,15 +21,15 @@ protected function getRule(): Rule true, false, true, - false + 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.', @@ -38,7 +39,46 @@ public function testRule(): void 'Anonymous function should return int but returns string.', 14, ], + + ]); + } + + public function testRuleNever(): void + { + 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 a800683d22..26b4a4b906 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -3,33 +3,44 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\NullsafeCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallCallablesRuleTest extends \PHPStan\Testing\RuleTestCase +class CallCallablesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, false, true); return new CallCallablesRule( new FunctionCallParametersCheck( $ruleLevelHelper, + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, true, true, true, - true ), $ruleLevelHelper, - true + true, ); } public function testRule(): void { - $this->analyse([__DIR__ . '/data/callables.php'], [ + $errors = [ [ 'Trying to invoke string but it might not be a callable.', 17, @@ -43,11 +54,11 @@ public function testRule(): void 25, ], [ - 'Parameter #1 $i of callable array($this(CallCallables\Foo), \'doBar\') expects int, string given.', + 'Parameter #1 $i of callable array{$this(CallCallables\\Foo), \'doBar\'} expects int, string given.', 33, ], [ - 'Callable array(\'CallCallables\\\\Foo\', \'doStaticBaz\') invoked with 1 parameter, 0 required.', + 'Callable array{\'CallCallables\\\\Foo\', \'doStaticBaz\'} invoked with 1 parameter, 0 required.', 39, ], [ @@ -85,42 +96,227 @@ public function testRule(): void [ 'Invoking callable on an unknown class CallCallables\Bar.', 90, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Parameter #1 ...$foo of closure expects CallCallables\Foo, array given.', 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 array(object, \'bar\') but it might not be a callable.', - 131, + '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.', + 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.', + 172, + ], + [ + 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', + 176, + ], + [ + 'Trying to invoke array{\'CallCallables\\\\CallableInForeach\', \'bar\'|\'foo\'} but it might not be a callable.', + 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) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/callables-named-arguments.php'], [ + [ + 'Missing parameter $j (int) in call to closure.', + 14, + ], + [ + 'Unknown parameter $i in call to callable callable(int, int): void.', + 23, + ], + [ + 'Missing parameter $ (int) in call to callable callable(int, int): void.', + 23, + ], + [ + 'Missing parameter $j (int) in call to callable callable(int, int): void.', + 24, + ], + [ + 'Unknown parameter $z in call to callable callable(int, int): void.', + 25, + ], + ]); + } + + public function dataBug3566(): array + { + return [ + [ + true, + [ + [ + 'Parameter #1 of closure expects int, TMemberType given.', + 29, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @dataProvider dataBug3566 + * @param list $errors + */ + public function testBug3566(bool $checkExplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/bug-3566.php'], $errors); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/callables-nullsafe.php'], [ + [ + 'Parameter #1 $val of closure expects int, int|null given.', + 18, ], + ]); + } + + public function testBug1849(): void + { + $this->analyse([__DIR__ . '/data/bug-1849.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->analyse([__DIR__ . '/data/call-first-class-callables.php'], [ [ - 'Trying to invoke array(object, \'yo\') but it might not be a callable.', - 163, + 'Unable to resolve the template type T in call to closure', + 14, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Trying to invoke array(object, \'yo\') but it might not be a callable.', - 167, + 'Unable to resolve the template type T in call to closure', + 17, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], + ]); + } + + public function testBug6701(): void + { + $this->analyse([__DIR__ . '/data/bug-6701.php'], [ + [ + 'Parameter #1 $test of closure expects string|null, int given.', + 14, + ], + [ + 'Parameter #1 $test of closure expects string|null, int given.', + 18, + ], + [ + 'Parameter #1 $test of closure expects string|null, int given.', + 24, + ], + ]); + } + + 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'], [ [ - 'Trying to invoke array(\'CallCallables\\\\CallableInForeach\', \'bar\'|\'foo\') but it might not be a callable.', - 179, + '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 e9defacaac..409535685f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3,20 +3,31 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\NullsafeCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use function sprintf; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToFunctionParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class CallToFunctionParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), 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), ); } @@ -28,6 +39,10 @@ public function testCallToFunctionWithoutParameters(): void public function testCallToFunctionWithIncorrectParameters(): void { + $setErrorHandlerError = PHP_VERSION_ID < 80000 + ? 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.' + : 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.'; + require_once __DIR__ . '/data/incorrect-call-to-function-definition.php'; $this->analyse([__DIR__ . '/data/incorrect-call-to-function.php'], [ [ @@ -43,7 +58,7 @@ public function testCallToFunctionWithIncorrectParameters(): void 14, ], [ - 'Parameter #1 $error_handler of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.', + $setErrorHandlerError, 16, ], ]); @@ -131,74 +146,134 @@ public function testCallToArrayMapVariadic(): void public function testCallToWeirdFunctions(): void { - $this->analyse([__DIR__ . '/data/call-to-weird-functions.php'], [ - [ - 'Function implode invoked with 0 parameters, 1-2 required.', - 3, - ], - [ - 'Function implode invoked with 3 parameters, 1-2 required.', - 6, - ], - [ - 'Function strtok invoked with 0 parameters, 1-2 required.', - 8, - ], - [ - 'Function strtok invoked with 3 parameters, 1-2 required.', - 11, - ], - [ - 'Function fputcsv invoked with 1 parameter, 2-5 required.', - 12, - ], - [ - 'Function imagepng invoked with 0 parameters, 1-4 required.', - 16, - ], - [ - 'Function imagepng invoked with 5 parameters, 1-4 required.', - 19, - ], - [ - 'Function locale_get_display_language invoked with 3 parameters, 1-2 required.', - 30, - ], - [ - 'Function mysqli_fetch_all invoked with 0 parameters, 1-2 required.', - 32, - ], - [ - 'Function mysqli_fetch_all invoked with 3 parameters, 1-2 required.', - 35, - ], - [ - 'Function openssl_open invoked with 7 parameters, 4-6 required.', - 37, - ], - [ - 'Function openssl_x509_parse invoked with 3 parameters, 1-2 required.', - 41, - ], - [ - 'Function openssl_pkcs12_export invoked with 6 parameters, 4-5 required.', - 47, - ], - [ - 'Parameter #1 $depth of function xdebug_call_class expects int, string given.', - 49, - ], - ]); + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Function implode invoked with 0 parameters, 1-2 required.', + 3, + ], + [ + 'Function implode invoked with 3 parameters, 1-2 required.', + 6, + ], + [ + 'Function strtok invoked with 0 parameters, 1-2 required.', + 8, + ], + [ + 'Function strtok invoked with 3 parameters, 1-2 required.', + 11, + ], + [ + sprintf('Function fputcsv invoked with 1 parameter, 2-%d required.', PHP_VERSION_ID >= 80100 ? 6 : 5), + 12, + ], + [ + 'Function imagepng invoked with 0 parameters, 1-4 required.', + 16, + ], + [ + 'Function imagepng invoked with 5 parameters, 1-4 required.', + 19, + ], + [ + 'Function locale_get_display_language invoked with 3 parameters, 1-2 required.', + 30, + ], + [ + 'Function mysqli_fetch_all invoked with 0 parameters, 1-2 required.', + 32, + ], + [ + 'Function mysqli_fetch_all invoked with 3 parameters, 1-2 required.', + 35, + ], + [ + 'Function openssl_open invoked with 4 parameters, 5-6 required.', + 38, + ], + [ + 'Function openssl_open invoked with 7 parameters, 5-6 required.', + 39, + ], + [ + 'Function openssl_x509_parse invoked with 3 parameters, 1-2 required.', + 43, + ], + [ + 'Function openssl_pkcs12_export invoked with 6 parameters, 4-5 required.', + 49, + ], + [ + 'Parameter #1 $depth of function xdebug_call_class expects int, string given.', + 51, + ], + ]; + } else { + $errors = [ + [ + 'Function implode invoked with 0 parameters, 1-2 required.', + 3, + ], + [ + 'Function implode invoked with 3 parameters, 1-2 required.', + 6, + ], + [ + 'Function strtok invoked with 0 parameters, 1-2 required.', + 8, + ], + [ + 'Function strtok invoked with 3 parameters, 1-2 required.', + 11, + ], + [ + 'Function fputcsv invoked with 1 parameter, 2-5 required.', + 12, + ], + [ + 'Function imagepng invoked with 0 parameters, 1-4 required.', + 16, + ], + [ + 'Function imagepng invoked with 5 parameters, 1-4 required.', + 19, + ], + [ + 'Function locale_get_display_language invoked with 3 parameters, 1-2 required.', + 30, + ], + [ + 'Function mysqli_fetch_all invoked with 0 parameters, 1-2 required.', + 32, + ], + [ + 'Function mysqli_fetch_all invoked with 3 parameters, 1-2 required.', + 35, + ], + [ + 'Function openssl_open invoked with 7 parameters, 4-6 required.', + 39, + ], + [ + 'Function openssl_x509_parse invoked with 3 parameters, 1-2 required.', + 43, + ], + [ + 'Function openssl_pkcs12_export invoked with 6 parameters, 4-5 required.', + 49, + ], + [ + 'Parameter #1 $depth of function xdebug_call_class expects int, string given.', + 51, + ], + ]; + } + $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.', @@ -220,18 +295,18 @@ public function testPassingNonVariableToParameterPassedByReference(): void 33, ], [ - 'Parameter #1 $array_arg of function reset expects array, null given.', + '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.', @@ -242,8 +317,13 @@ public function testImplodeOnPhp74(): void 8, ], ]; - if (PHP_VERSION_ID < 70400) { - $errors = []; + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Parameter #2 $array of function implode expects array|null, string given.', + 8, + ], + ]; } $this->analyse([__DIR__ . '/data/implode-74.php'], $errors); @@ -251,12 +331,14 @@ 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 >= 70400) { + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Parameter #2 $array of function implode expects array|null, string given.', + 8, + ], + ]; + } else { $errors = [ [ 'Parameter #1 $glue of function implode expects string, array given.', @@ -311,33 +393,50 @@ public function testUnpackOperator(): void { $this->analyse([__DIR__ . '/data/unpack-operator.php'], [ [ - 'Parameter #2 ...$args of function sprintf expects bool|float|int|string|null, array given.', + 'Parameter #2 ...$values of function sprintf expects bool|float|int|string|null, array given.', 18, ], [ - 'Parameter #2 ...$args of function sprintf expects bool|float|int|string|null, array given.', + 'Parameter #2 ...$values of function sprintf expects bool|float|int|string|null, array given.', 19, ], [ - 'Parameter #2 ...$args of function sprintf expects bool|float|int|string|null, UnpackOperator\Foo given.', + 'Parameter #2 ...$values of function sprintf expects bool|float|int|string|null, UnpackOperator\Foo given.', 22, ], [ - 'Parameter #2 ...$args of function printf expects bool|float|int|string|null, UnpackOperator\Foo given.', + 'Parameter #2 ...$values of function printf expects bool|float|int|string|null, UnpackOperator\Foo given.', 24, ], ]); } - public function testFunctionWithNumericParameterThatCreatedByAddition(): void + public function testFputCsv(): void { - $this->analyse([__DIR__ . '/data/function-with-int-parameter-that-created-by-addition.php'], [ + $this->analyse([__DIR__ . '/data/fputcsv-fields-parameter.php'], [ [ - 'Parameter #1 $decimal_number of function dechex expects int, float|int given.', - 9, + 'Parameter #2 $fields of function fputcsv expects array, array given.', + 35, ], + ]); + } + + public function testPutCsvWithStringable(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test skipped on lower version than 8.0 (needs Stringable interface, added in PHP8)'); + } + + $this->analyse([__DIR__ . '/data/fputcsv-fields-parameter-php8.php'], [ + // No issues expected + ]); + } + + public function testFunctionWithNumericParameterThatCreatedByAddition(): void + { + $this->analyse([__DIR__ . '/data/function-with-int-parameter-that-created-by-addition.php'], [ [ - 'Parameter #1 $decimal_number of function dechex expects int, float|int given.', + 'Parameter #1 $num of function dechex expects int, float|int given.', 40, ], ]); @@ -361,10 +460,7 @@ public function testGenericFunction(): void [ 'Unable to resolve the template type A in call to function CallGenericFunction\f', 15, - ], - [ - 'Unable to resolve the template type B in call to function CallGenericFunction\f', - 15, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ 'Parameter #1 $a of function CallGenericFunction\g expects DateTime, DateTimeImmutable given.', @@ -373,6 +469,1775 @@ public function testGenericFunction(): void [ 'Unable to resolve the template type A in call to function CallGenericFunction\g', 26, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + ]); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $errors = [ + [ + 'Missing parameter $j (int) in call to function FunctionNamedArguments\foo.', + 7, + ], + [ + 'Unknown parameter $z in call to function FunctionNamedArguments\foo.', + 8, + ], + [ + 'Unknown parameter $a 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'], []); + } + + public function testBug4530(): void + { + $this->analyse([__DIR__ . '/data/bug-4530.php'], []); + } + + public function testBug2268(): void + { + require_once __DIR__ . '/data/bug-2268.php'; + $this->analyse([__DIR__ . '/data/bug-2268.php'], []); + } + + public function testBug2434(): void + { + require_once __DIR__ . '/data/bug-2434.php'; + $this->analyse([__DIR__ . '/data/bug-2434.php'], []); + } + + public function testBug2846(): void + { + $this->analyse([__DIR__ . '/data/bug-2846.php'], []); + } + + 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'], []); + } + + public function testBugNumberFormatNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/number-format-named-arguments.php'], []); + } + + public function testArrayReduceCallback(): void + { + $this->analyse([__DIR__ . '/data/array_reduce.php'], [ + [ + '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(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(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 + { + $this->analyse([__DIR__ . '/data/array_reduce_arrow.php'], [ + [ + '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(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(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.', + ], + ]); + } + + public function testArrayWalkCallback(): void + { + $this->analyse([__DIR__ . '/data/array_walk.php'], [ + [ + '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(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 + { + $this->analyse([__DIR__ . '/data/array_walk_arrow.php'], [ + [ + '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(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, + ], + ]); + } + + 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.', + 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 mb_ereg_replace_callback expects callable(array): string, Closure(string): string given.', + 13, + ], + [ + 'Parameter #2 $callback of function mb_ereg_replace_callback expects callable(array): string, Closure(array): void given.', + 20, + ], + [ + 'Parameter #2 $callback of function mb_ereg_replace_callback expects callable(array): string, Closure(): void given.', + 25, + ], + ]); + } + + public function testUasortCallback(): void + { + $this->analyse([__DIR__ . '/data/uasort.php'], [ + [ + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', + 7, + ], + ]); + } + + public function testUasortArrowFunctionCallback(): void + { + $this->analyse([__DIR__ . '/data/uasort_arrow.php'], [ + [ + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', + 7, + ], + ]); + } + + public function testUsortCallback(): void + { + $this->analyse([__DIR__ . '/data/usort.php'], [ + [ + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', + 14, + ], + ]); + } + + public function testUsortArrowFunctionCallback(): void + { + $this->analyse([__DIR__ . '/data/usort_arrow.php'], [ + [ + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', + 14, + ], + ]); + } + + public function testUksortCallback(): void + { + $this->analyse([__DIR__ . '/data/uksort.php'], [ + [ + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', + 14, + ], + [ + 'Parameter #2 $callback of function uksort expects callable(int, int): int, Closure(string, string): 1 given.', + 50, + ], + ]); + } + + public function testUksortArrowFunctionCallback(): void + { + $this->analyse([__DIR__ . '/data/uksort_arrow.php'], [ + [ + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', + 14, + ], + [ + 'Parameter #2 $callback of function uksort expects callable(int, int): int, Closure(string, string): 1 given.', + 44, + ], + ]); + } + + public function testVaryingAcceptor(): void + { + require_once __DIR__ . '/data/varying-acceptor.php'; + $this->analyse([__DIR__ . '/data/varying-acceptor.php'], [ + [ + 'Parameter #1 $closure of function VaryingAcceptor\bar expects callable(callable(): string): string, callable(callable(): int): string given.', + 17, + ], + ]); + } + + public function testBug3660(): void + { + $this->analyse([__DIR__ . '/data/bug-3660.php'], [ + [ + 'Parameter #1 $string of function strlen expects string, int given.', + 7, + ], + [ + 'Parameter #1 $string of function strlen expects string, int given.', + 8, + ], + ]); + } + + public function testExplode(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/explode-80.php'], [ + [ + 'Parameter #1 $separator of function explode expects non-empty-string, string given.', + 14, + ], + [ + 'Parameter #1 $separator of function explode expects non-empty-string, \'\' given.', + 16, + ], + [ + 'Parameter #1 $separator of function explode expects non-empty-string, 1 given.', + 17, + ], + ]); + } + + public function testProcOpen(): void + { + $this->analyse([__DIR__ . '/data/proc_open.php'], [ + [ + "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.", + ], + ]); + } + + public function testBug5609(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5609.php'], []); + } + + public function dataArrayMapMultiple(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataArrayMapMultiple + */ + public function testArrayMapMultiple(bool $checkExplicitMixed): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/array_map_multiple.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(1|2, \'bar\'|\'foo\'): mixed)|null, Closure(int, int): void given.', + 58, + ], + ]); + } + + public function dataArrayFilterCallback(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataArrayFilterCallback + */ + public function testArrayFilterCallback(bool $checkExplicitMixed): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $errors = [ + [ + 'Parameter #2 $callback of function array_filter expects (callable(int): bool)|null, Closure(string): true given.', + 17, + ], + ]; + if ($checkExplicitMixed) { + $errors[] = [ + '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 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 < 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.', + 13, + ], + [ + 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', + 21, + ], + ]); + } + + public function testBug1954(): void + { + $this->analyse([__DIR__ . '/data/bug-1954.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(1|stdClass): mixed)|null, Closure(string): string given.', + 7, + ], + ]); + } + + public function testBug2782(): void + { + $this->analyse([__DIR__ . '/data/bug-2782.php'], [ + [ + 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): (-1|1) given.', + 13, + ], + ]); + } + + public function testBug5661(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5661.php'], []); + } + + public function testBug5872(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5872.php'], [ + [ + 'Parameter #2 $array of function array_map expects array, mixed given.', + 12, + ], + ]); + } + + public function testBug5834(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5834.php'], []); + } + + public function testBug5881(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5881.php'], []); + } + + public function testBug5861(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5861.php'], []); + } + + public function testCallUserFuncArray(): void + { + if (PHP_VERSION_ID >= 80000) { + $errors = []; + } else { + $errors = [ + [ + 'Parameter #2 $parameters of function call_user_func_array expects array, array> given.', + 3, + ], + ]; + } + $this->analyse([__DIR__ . '/data/call-user-func-array.php'], $errors); + } + + public function testFirstClassCallables(): void + { + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-callables.php'], []); + } + + public function testBug4413(): void + { + require_once __DIR__ . '/data/bug-4413.php'; + $this->analyse([__DIR__ . '/data/bug-4413.php'], [ + [ + 'Parameter #1 $date of function Bug4413\takesDate expects class-string, string given.', + 18, + ], + ]); + } + + 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/CallToFunctionStamentWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRuleTest.php deleted file mode 100644 index 01c6feb53e..0000000000 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class CallToFunctionStamentWithoutSideEffectsRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new CallToFunctionStamentWithoutSideEffectsRule($this->createReflectionProvider()); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects.php'], [ - [ - 'Call to function sprintf() on a separate line has no effect.', - 11, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php new file mode 100644 index 0000000000..85c1f400ee --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -0,0 +1,116 @@ + + */ +class CallToFunctionStatementWithoutSideEffectsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithoutSideEffectsRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects.php'], [ + [ + '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, + ], + ]); + } + + public function testPhpDoc(): void + { + require_once __DIR__ . '/data/function-call-statement-no-side-effects-phpdoc-definition.php'; + $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects-phpdoc.php'], [ + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure1() on a separate line has no effect.', + 8, + ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure2() on a separate line has no effect.', + 9, + ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure3() on a separate line has no effect.', + 10, + ], + [ + '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, + ], + ]); + } + + public function testBug4455(): void + { + require_once __DIR__ . '/data/bug-4455.php'; + $this->analyse([__DIR__ . '/data/bug-4455.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->analyse([__DIR__ . '/data/first-class-callable-function-without-side-effect.php'], [ + [ + 'Call to function mkdir() on a separate line has no effect.', + 12, + ], + [ + 'Call to function strlen() on a separate line has no effect.', + 24, + ], + [ + 'Call to function FirstClassCallableFunctionWithoutSideEffect\foo() on a separate line has no effect.', + 36, + ], + [ + 'Call to function FirstClassCallableFunctionWithoutSideEffect\bar() on a separate line has no effect.', + 49, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 7249213abb..6707068e04 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -2,15 +2,24 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToNonExistentFunctionRuleTest extends \PHPStan\Testing\RuleTestCase +class CallToNonExistentFunctionRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule + { + return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true, true); + } + + public function shouldNarrowMethodScopeFromConstructor(): bool { - return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true); + return true; } public function testEmptyFile(): void @@ -30,6 +39,7 @@ public function testCallToNonexistentFunction(): void [ 'Function foobarNonExistentFunction not found.', 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); } @@ -40,6 +50,7 @@ public function testCallToNonexistentNestedFunction(): void [ 'Function barNonExistentFunction not found.', 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); } @@ -63,4 +74,204 @@ public function testCallToIncorrectCaseFunctionName(): void ]); } + public function testMatchExprAnalysis(): void + { + $this->analyse([__DIR__ . '/data/match-expr-analysis.php'], [ + [ + 'Function lorem not found.', + 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ipsum not found.', + 11, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function dolor not found.', + 11, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function sit not found.', + 12, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + 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) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/create_function.php'], [ + [ + 'Function create_function not found.', + 4, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testCreateFunctionPhp7(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.x.'); + } + + $this->analyse([__DIR__ . '/data/create_function.php'], []); + } + + public function testBug3576(): void + { + $this->analyse([__DIR__ . '/data/bug-3576.php'], [ + [ + 'Function bug3576 not found.', + 14, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 17, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 29, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 38, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 41, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + 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 new file mode 100644 index 0000000000..360ed89a3c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -0,0 +1,60 @@ + + */ +class ClosureAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new ClosureAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/closure-attributes.php'], [ + [ + 'Attribute class ClosureAttributes\Foo does not have the function target.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index 1a2e6294d6..938020a0ba 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -3,17 +3,19 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ClosureReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ClosureReturnTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false))); + return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true))); } public function testClosureReturnTypeRule(): void @@ -28,17 +30,33 @@ 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, ], + [ + 'Anonymous function should return array{}|null but empty return statement found.', + 88, + ], + [ + 'Anonymous function should return string but returns int.', + 105, + ], + [ + 'Anonymous function should return string but returns int.', + 115, + ], + [ + 'Anonymous function should return string but returns int.', + 118, + ], ]); } @@ -70,4 +88,49 @@ public function testClosureReturnTypePhp71Typehints(): void ]); } + 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/ClosureUsesThisRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureUsesThisRuleTest.php deleted file mode 100644 index a84faa310a..0000000000 --- a/tests/PHPStan/Rules/Functions/ClosureUsesThisRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class ClosureUsesThisRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new ClosureUsesThisRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/closure-uses-this.php'], [ - [ - 'Anonymous function uses $this assigned to variable $that. Use $this directly in the function body.', - 16, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Functions/DefineParametersRuleTest.php b/tests/PHPStan/Rules/Functions/DefineParametersRuleTest.php new file mode 100644 index 0000000000..d78909627d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/DefineParametersRuleTest.php @@ -0,0 +1,35 @@ + + */ +class DefineParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DefineParametersRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testFile(): void + { + if (PHP_VERSION_ID < 80000) { + $this->analyse([__DIR__ . '/data/call-to-define.php'], []); + } else { + $this->analyse([__DIR__ . '/data/call-to-define.php'], [ + [ + 'Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported.', + 3, + ], + ]); + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/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 d000474152..158d763831 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -2,36 +2,314 @@ namespace PHPStan\Rules\Functions; +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; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInArrowFunctionTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private int $phpVersionId = PHP_VERSION_ID; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), 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 typehint type ArrowFunctionExistingClassesInTypehints\Bar.', + 'Parameter $bar of anonymous function has invalid type ArrowFunctionExistingClassesInTypehints\Bar.', 10, ], [ - 'Return typehint of anonymous function has invalid type ArrowFunctionExistingClassesInTypehints\Baz.', + 'Anonymous function has invalid return type ArrowFunctionExistingClassesInTypehints\Baz.', 10, ], ]); } + public function dataNativeUnionTypes(): array + { + return [ + [ + 70400, + [ + [ + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 23, + ], + [ + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 24, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataNativeUnionTypes + * @param list $errors + */ + public function testNativeUnionTypes(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); + } + + 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, + [ + [ + '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.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, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRequiredParameterAfterOptional + * @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-arrow.php'], $errors); + } + + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 27, + ], + [ + 'Anonymous function has unresolvable native return type.', + 27, + ], + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 29, + ], + [ + 'Anonymous function has unresolvable native return type.', + 29, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param list $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $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 68ec99cc99..988b42b6a9 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -2,30 +2,53 @@ namespace PHPStan\Rules\Functions; +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; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInClosureTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private int $phpVersionId = PHP_VERSION_ID; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), 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 { $this->analyse([__DIR__ . '/data/closure-typehints.php'], [ [ - 'Return typehint of anonymous function has invalid type TestClosureFunctionTypehints\NonexistentClass.', + 'Anonymous function has invalid return type TestClosureFunctionTypehints\NonexistentClass.', 10, ], [ - 'Parameter $bar of anonymous function has invalid typehint type TestClosureFunctionTypehints\BarFunctionTypehints.', + 'Parameter $bar of anonymous function has invalid type TestClosureFunctionTypehints\BarFunctionTypehints.', 15, ], [ @@ -37,11 +60,11 @@ public function testExistingClassInTypehint(): void 30, ], [ - 'Parameter $trait of anonymous function has invalid typehint type TestClosureFunctionTypehints\SomeTrait.', + 'Parameter $trait of anonymous function has invalid type TestClosureFunctionTypehints\SomeTrait.', 45, ], [ - 'Return typehint of anonymous function has invalid type TestClosureFunctionTypehints\SomeTrait.', + 'Anonymous function has invalid return type TestClosureFunctionTypehints\SomeTrait.', 50, ], ]); @@ -51,19 +74,16 @@ public function testValidTypehintPhp71(): void { $this->analyse([__DIR__ . '/data/closure-7.1-typehints.php'], [ [ - 'Parameter $bar of anonymous function has invalid typehint type TestClosureFunctionTypehintsPhp71\NonexistentClass.', + 'Parameter $bar of anonymous function has invalid type TestClosureFunctionTypehintsPhp71\NonexistentClass.', 35, ], [ - 'Return typehint of anonymous function has invalid type TestClosureFunctionTypehintsPhp71\NonexistentClass.', + 'Anonymous function has invalid return type TestClosureFunctionTypehintsPhp71\NonexistentClass.', 35, ], ]); } - /** - * @requires PHP 7.2 - */ public function testValidTypehintPhp72(): void { $this->analyse([__DIR__ . '/data/closure-7.2-typehints.php'], []); @@ -71,15 +91,270 @@ 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 typehint type void.', + 'Parameter $param of anonymous function has invalid type void.', 5, ], ]); } + public function dataNativeUnionTypes(): array + { + return [ + [ + 70400, + [ + [ + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 15, + ], + [ + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 19, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataNativeUnionTypes + * @param list $errors + */ + public function testNativeUnionTypes(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); + } + + 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, + [ + [ + '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.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, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRequiredParameterAfterOptional + * @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); + } + + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 30, + ], + [ + 'Anonymous function has unresolvable native return type.', + 30, + ], + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 35, + ], + [ + 'Anonymous function has unresolvable native return type.', + 35, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param list $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $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 2f5ebd63ef..f03764a657 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -2,19 +2,42 @@ namespace PHPStan\Rules\Functions; +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; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInTypehintsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private int $phpVersionId = PHP_VERSION_ID; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), 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 @@ -22,15 +45,15 @@ public function testExistingClassInTypehint(): void require_once __DIR__ . '/data/typehints.php'; $this->analyse([__DIR__ . '/data/typehints.php'], [ [ - 'Return typehint of function TestFunctionTypehints\foo() has invalid type TestFunctionTypehints\NonexistentClass.', + 'Function TestFunctionTypehints\foo() has invalid return type TestFunctionTypehints\NonexistentClass.', 15, ], [ - 'Parameter $bar of function TestFunctionTypehints\bar() has invalid typehint type TestFunctionTypehints\BarFunctionTypehints.', + 'Parameter $bar of function TestFunctionTypehints\bar() has invalid type TestFunctionTypehints\BarFunctionTypehints.', 20, ], [ - 'Return typehint of function TestFunctionTypehints\returnParent() has invalid type TestFunctionTypehints\parent.', + 'Function TestFunctionTypehints\returnParent() has invalid return type TestFunctionTypehints\parent.', 33, ], [ @@ -58,29 +81,33 @@ public function testExistingClassInTypehint(): void 56, ], [ - 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInNative() has invalid typehint type TestFunctionTypehints\SomeTrait.', + 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInNative() has invalid type TestFunctionTypehints\SomeTrait.', 61, ], [ - 'Return typehint of function TestFunctionTypehints\referencesTraitsInNative() has invalid type TestFunctionTypehints\SomeTrait.', + 'Function TestFunctionTypehints\referencesTraitsInNative() has invalid return type TestFunctionTypehints\SomeTrait.', 61, ], [ - 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid typehint type TestFunctionTypehints\SomeTrait.', + 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid type TestFunctionTypehints\SomeTrait.', 70, ], [ - 'Return typehint of function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid type TestFunctionTypehints\SomeTrait.', + 'Function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid return type TestFunctionTypehints\SomeTrait.', 70, ], [ - 'Parameter $string of function TestFunctionTypehints\genericClassString() has invalid typehint type TestFunctionTypehints\SomeNonexistentClass.', + 'Parameter $string of function TestFunctionTypehints\genericClassString() has invalid type TestFunctionTypehints\SomeNonexistentClass.', 78, ], [ - 'Parameter $string of function TestFunctionTypehints\genericTemplateClassString() has invalid typehint type TestFunctionTypehints\SomeNonexistentClass.', + 'Parameter $string of function TestFunctionTypehints\genericTemplateClassString() has invalid type TestFunctionTypehints\SomeNonexistentClass.', 87, ], + [ + 'Template type T of function TestFunctionTypehints\templateTypeMissingInParameter() is not referenced in a parameter.', + 96, + ], ]); } @@ -89,15 +116,15 @@ public function testWithoutNamespace(): void require_once __DIR__ . '/data/typehintsWithoutNamespace.php'; $this->analyse([__DIR__ . '/data/typehintsWithoutNamespace.php'], [ [ - 'Return typehint of function fooWithoutNamespace() has invalid type NonexistentClass.', + 'Function fooWithoutNamespace() has invalid return type NonexistentClass.', 13, ], [ - 'Parameter $bar of function barWithoutNamespace() has invalid typehint type BarFunctionTypehints.', + 'Parameter $bar of function barWithoutNamespace() has invalid type BarFunctionTypehints.', 18, ], [ - 'Return typehint of function returnParentWithoutNamespace() has invalid type parent.', + 'Function returnParentWithoutNamespace() has invalid return type parent.', 31, ], [ @@ -125,19 +152,19 @@ public function testWithoutNamespace(): void 54, ], [ - 'Parameter $trait of function referencesTraitsInNativeWithoutNamespace() has invalid typehint type SomeTraitWithoutNamespace.', + 'Parameter $trait of function referencesTraitsInNativeWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', 59, ], [ - 'Return typehint of function referencesTraitsInNativeWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', + 'Function referencesTraitsInNativeWithoutNamespace() has invalid return type SomeTraitWithoutNamespace.', 59, ], [ - 'Parameter $trait of function referencesTraitsInPhpDocWithoutNamespace() has invalid typehint type SomeTraitWithoutNamespace.', + 'Parameter $trait of function referencesTraitsInPhpDocWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', 68, ], [ - 'Return typehint of function referencesTraitsInPhpDocWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', + 'Function referencesTraitsInPhpDocWithoutNamespace() has invalid return type SomeTraitWithoutNamespace.', 68, ], ]); @@ -145,15 +172,338 @@ 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 typehint type void.', + 'Parameter $param of function VoidParameterTypehint\doFoo() has invalid type void.', 9, ], ]); } + public function dataNativeUnionTypes(): array + { + return [ + [ + 70400, + [ + [ + 'Function NativeUnionTypesSupport\foo() uses native union types but they\'re supported only on PHP 8.0 and later.', + 5, + ], + [ + 'Function NativeUnionTypesSupport\bar() uses native union types but they\'re supported only on PHP 8.0 and later.', + 10, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataNativeUnionTypes + * @param list $errors + */ + public function testNativeUnionTypes(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); + } + + 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, + [ + [ + '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.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, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRequiredParameterAfterOptional + * @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); + } + + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of function FunctionIntersectionTypes\doBar() has unresolvable native type.', + 30, + ], + [ + 'Function FunctionIntersectionTypes\doBar() has unresolvable native return type.', + 30, + ], + [ + 'Parameter $a of function FunctionIntersectionTypes\doBaz() has unresolvable native type.', + 35, + ], + [ + 'Function FunctionIntersectionTypes\doBaz() has unresolvable native return type.', + 35, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param list $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $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 new file mode 100644 index 0000000000..2fc38ca188 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -0,0 +1,60 @@ + + */ +class FunctionAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new FunctionAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/function-attributes.php'], [ + [ + 'Attribute class FunctionAttributes\Foo does not have the function target.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php new file mode 100644 index 0000000000..18bd5ed206 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php @@ -0,0 +1,78 @@ + + */ +class FunctionCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new FunctionCallableRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new PhpVersion(PHP_VERSION_ID), + true, + true, + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + $this->analyse([__DIR__ . '/data/function-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/function-callable.php'], [ + [ + 'Function nonexistent not found.', + 13, + ], + [ + 'Creating callable from string but it might not be a callable.', + 19, + ], + [ + 'Creating callable from 1 but it\'s not a callable.', + 33, + ], + [ + 'Call to function strlen() with incorrect case: StrLen', + 38, + ], + [ + 'Creating callable from 1|(callable(): mixed) but it might not be a callable.', + 47, + ], + [ + 'Creating callable from an unknown class FunctionCallable\Nonexistent.', + 52, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/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/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php index 6dd5a8e4be..56870a1d42 100644 --- a/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class IncompatibleDefaultParameterTypeRuleTest extends RuleTestCase { diff --git a/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php index fe6fb38f23..49ca5741bf 100644 --- a/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InnerFunctionRuleTest extends \PHPStan\Testing\RuleTestCase +class InnerFunctionRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InnerFunctionRule(); } diff --git a/tests/PHPStan/Rules/Functions/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 4c54b190cb..c88fb550da 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -3,17 +3,18 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingFunctionParameterTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingFunctionParameterTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck($broker, true, true)); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -21,61 +22,76 @@ public function testRule(): void require_once __DIR__ . '/data/missing-function-parameter-typehint.php'; $this->analyse([__DIR__ . '/data/missing-function-parameter-typehint.php'], [ [ - 'Function globalFunction() has parameter $b with no typehint specified.', + 'Function globalFunction() has parameter $b with no type specified.', 9, ], [ - 'Function globalFunction() has parameter $c with no typehint specified.', + 'Function globalFunction() has parameter $c with no type specified.', 9, ], [ - 'Function MissingFunctionParameterTypehint\namespacedFunction() has parameter $d with no typehint specified.', + 'Function MissingFunctionParameterTypehint\namespacedFunction() has parameter $d with no type specified.', 24, ], [ 'Function MissingFunctionParameterTypehint\missingArrayTypehint() has parameter $a with no value type specified in iterable type array.', 36, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingPhpDocIterableTypehint() has parameter $a with no value type specified in iterable type array.', 44, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\unionTypeWithUnknownArrayValueTypehint() has parameter $a with no value type specified in iterable type array.', 60, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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, - "Consider adding something like iterable to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingIterableTypehintPhpDoc() has parameter $iterable with no value type specified in iterable type iterable.', 143, - "Consider adding something like iterable to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingTraversableTypehint() has parameter $traversable with no value type specified in iterable type Traversable.', 148, - "Consider adding something like Traversable to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], [ 'Function MissingFunctionParameterTypehint\missingTraversableTypehintPhpDoc() has parameter $traversable with no value type specified in iterable type Traversable.', 156, - "Consider adding something like Traversable to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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 36a684b9b2..2b64aba5ba 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php @@ -3,17 +3,18 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingFunctionReturnTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingFunctionReturnTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck($broker, true, true)); + return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void @@ -21,27 +22,41 @@ public function testRule(): void require_once __DIR__ . '/data/missing-function-return-typehint.php'; $this->analyse([__DIR__ . '/data/missing-function-return-typehint.php'], [ [ - 'Function globalFunction1() has no return typehint specified.', + 'Function globalFunction1() has no return type specified.', 5, ], [ - 'Function MissingFunctionReturnTypehint\namespacedFunction1() has no return typehint specified.', + 'Function MissingFunctionReturnTypehint\namespacedFunction1() has no return type specified.', 30, ], [ 'Function MissingFunctionReturnTypehint\unionTypeWithUnknownArrayValueTypehint() return type has no value type specified in iterable type array.', 51, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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, + ], + [ + 'Function MissingFunctionReturnTypehint\closureWithNoPrototype() return type has no signature specified for Closure.', + 113, + ], + [ + 'Function MissingFunctionReturnTypehint\callableWithNoPrototype() return type has no signature specified for callable.', + 127, + ], + [ + 'Function MissingFunctionReturnTypehint\callableNestedNoPrototype() return type has no signature specified for callable.', + 141, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php new file mode 100644 index 0000000000..7f8e3cef19 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -0,0 +1,78 @@ + + */ +class ParamAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new ParamAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/param-attributes.php'], [ + [ + 'Attribute class ParamAttributes\Foo does not have the parameter target.', + 33, + ], + [ + 'Attribute class ParamAttributes\Foo does not have the parameter or property target.', + 72, + ], + [ + 'Attribute class ParamAttributes\Qux does not have the parameter target.', + 82, + ], + ]); + } + + 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 7e3eb93104..252f2919ec 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -2,15 +2,23 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class PrintfParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class PrintfParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new PrintfParametersRule(); + return new PrintfParametersRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + $this->createReflectionProvider(), + ); } public function testFile(): void @@ -91,4 +99,28 @@ public function testFile(): void ]); } + public function testBug4717(): void + { + $errors = [ + [ + 'Call to sprintf contains 1 placeholder, 2 values given.', + 5, + ], + ]; + if (PHP_VERSION_ID >= 80000) { + $errors = []; + } + $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 06d0f8a03a..40c0526e25 100644 --- a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php @@ -2,20 +2,25 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_INT_SIZE; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class RandomIntParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class RandomIntParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new RandomIntParametersRule($this->createReflectionProvider(), true); + return new RandomIntParametersRule($this->createReflectionProvider(), new PhpVersion(80000), true); } public function testFile(): void { - $this->analyse([__DIR__ . '/data/random-int.php'], [ + $expectedErrors = [ [ 'Parameter #1 $min (1) of function random_int expects lower number than parameter #2 $max (0).', 8, @@ -52,7 +57,21 @@ public function testFile(): void 'Parameter #1 $min (int<0, 10>) of function random_int expects lower number than parameter #2 $max (int<0, 10>).', 31, ], - ]); + ]; + if (PHP_INT_SIZE === 4) { + // TODO: should fail on 64-bit in a similar fashion, guess it does not because of the union type + $expectedErrors[] = [ + 'Parameter #1 $min (2147483647) of function random_int expects lower number than parameter #2 $max (-2147483648).', + 33, + ]; + } + + $this->analyse([__DIR__ . '/data/random-int.php'], $expectedErrors); + } + + public function testBug6361(): void + { + $this->analyse([__DIR__ . '/data/bug-6361.php'], []); } } diff --git a/tests/PHPStan/Rules/Functions/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 new file mode 100644 index 0000000000..a3b8f1db9e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ReturnNullsafeByRefRuleTest.php @@ -0,0 +1,53 @@ + + */ +class ReturnNullsafeByRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReturnNullsafeByRefRule(new NullsafeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/return-null-safe-by-ref.php'], [ + [ + 'Nullsafe cannot be returned by reference.', + 15, + ], + [ + 'Nullsafe cannot be returned by reference.', + 25, + ], + [ + 'Nullsafe cannot be returned by reference.', + 36, + ], + ]); + } + + 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 820ef6bc8a..b5ca981736 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -3,23 +3,31 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ReturnTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkNullables; + + private bool $checkExplicitMixed; + + protected function getRule(): Rule { - [, $functionReflector] = self::getReflectors(); - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)), $functionReflector); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), $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.', @@ -57,11 +65,17 @@ public function testReturnTypeRule(): void 'Function ReturnTypes\returnVoidFromGenerator2() with return type void returns int but should not return anything.', 173, ], + [ + 'Function ReturnTypes\returnNever() should never return but return statement found.', + 181, + ], ]); } 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.', @@ -72,11 +86,277 @@ 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.', + 55, + ], + ]); + } + + 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->analyse([__DIR__ . '/data/is-generator.php'], []); + $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 16f7c7aa5b..38a555afda 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -2,17 +2,19 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class UnusedClosureUsesRuleTest extends \PHPStan\Testing\RuleTestCase +class UnusedClosureUsesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck()); + return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider(), true)); } public function testUnusedClosureUses(): void @@ -20,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_callback.php b/tests/PHPStan/Rules/Functions/data/array_filter_callback.php new file mode 100644 index 0000000000..96933c7e1a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_callback.php @@ -0,0 +1,25 @@ + $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_map_multiple.php b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php new file mode 100644 index 0000000000..0d96ce95e9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_map_multiple.php @@ -0,0 +1,68 @@ + + */ + protected $items = []; + + /** + * @param array $items + * @return void + */ + public function __construct($items) + { + $this->items = $items; + } + + /** + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return self + */ + public function map(callable $callback) + { + $keys = array_keys($this->items); + + $items = array_map($callback, $this->items, $keys); + + return new self(array_combine($keys, $items)); + } + + /** + * @return array + */ + public function all() + { + return $this->items; + } +} + +class Foo +{ + + public function doFoo(): void + { + array_map(function (int $a, string $b) { + + }, [1, 2], ['foo', 'bar']); + + array_map(function (int $a, int $b) { + + }, [1, 2], ['foo', 'bar']); + } + + public function arrayMapNull(): void + { + array_map(null, [1, 2], [3, 4]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/array_reduce.php b/tests/PHPStan/Rules/Functions/data/array_reduce.php new file mode 100644 index 0000000000..eb7588e9cc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_reduce.php @@ -0,0 +1,25 @@ + $foo . $current, + '' +); + +array_reduce( + [1,2,3], + fn(string $foo, int $current): string => $foo . $current, + null +); + + +array_reduce( + [1,2,3], + fn(string $foo, int $current): string => $foo . $current, +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff.php b/tests/PHPStan/Rules/Functions/data/array_udiff.php new file mode 100644 index 0000000000..23af63ccef --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff.php @@ -0,0 +1,39 @@ + $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.php b/tests/PHPStan/Rules/Functions/data/array_walk.php new file mode 100644 index 0000000000..eeebab63f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_walk.php @@ -0,0 +1,40 @@ + 1, 'bar' => 2]; +array_walk( + $array, + function(stdClass $in, float $key): string { + return ''; + } +); + +$array = ['foo' => 1, 'bar' => 2]; +array_walk( + $array, + function(int $in, string $key, int $extra): string { + return ''; + }, + 'extra' +); + +$array = ['foo' => 1, 'bar' => 2]; +array_walk( + $array, + function(int $value, string $key, int $extra): string { + return ''; + } +); + +function (): void { + $object = (object)['foo' => 'bar']; + array_walk($object, function ($v) { + return '_' . $v; + }); +}; + +function (): void { + $object = (object)['foo' => 'bar']; + array_walk_recursive($object, function ($v) { + return '_' . $v; + }); +}; diff --git a/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php new file mode 100644 index 0000000000..22a69296f1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_walk_arrow.php @@ -0,0 +1,20 @@ + 1, 'bar' => 2]; +array_walk( + $array, + fn(stdClass $in, float $key): string => '' +); + +$array = ['foo' => 1, 'bar' => 2]; +array_walk( + $array, + fn(int $in, string $key, int $extra): string => '', + 'extra' +); + +$array = ['foo' => 1, 'bar' => 2]; +array_walk( + $array, + fn(int $value, string $key, int $extra): string => '' +); diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php new file mode 100644 index 0000000000..30f1a93884 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-attributes.php @@ -0,0 +1,33 @@ + 1; + #[Bar] fn () => 1; + #[Baz] fn () => 1; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-intersection-types.php b/tests/PHPStan/Rules/Functions/data/arrow-function-intersection-types.php new file mode 100644 index 0000000000..246d426fb3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-intersection-types.php @@ -0,0 +1,29 @@ += 8.1 + +namespace ArrowFunctionIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +fn(Foo&Bar $a): Foo&Bar => 1; + +fn(Lorem&Ipsum $a): Lorem&Ipsum => 2; + +fn(int&mixed $a): int&mixed => 3; diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-never-return.php b/tests/PHPStan/Rules/Functions/data/arrow-function-never-return.php new file mode 100644 index 0000000000..5a9641fb06 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-never-return.php @@ -0,0 +1,15 @@ += 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-function-nullsafe-by-ref.php b/tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php new file mode 100644 index 0000000000..e78b724772 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php @@ -0,0 +1,15 @@ + $foo?->bar; +}; + +function (\stdClass $foo): void { + fn &() => $foo->bar; +}; + +function (\stdClass $foo): void { + fn () => $foo?->bar; +}; 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 281f500df8..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 + $this->doFoo(); + fn (?string $value): string => $value ?? '-'; + } + +} + +static fn (int $value): iterable => 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-1849.php b/tests/PHPStan/Rules/Functions/data/bug-1849.php new file mode 100644 index 0000000000..1978c8232f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-1849.php @@ -0,0 +1,11 @@ +real_connect( + null, + null, + null, + null, + null, + null, + \MYSQLI_CLIENT_SSL + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2568.php b/tests/PHPStan/Rules/Functions/data/bug-2568.php new file mode 100644 index 0000000000..f86417ab64 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2568.php @@ -0,0 +1,33 @@ + $arr + * @return array + */ +function my_array_keys($arr) { + return array_keys($arr); +} + +/** + * @template T of array-key + * + * @param array $arr + * @return array + */ +function my_array_keys2($arr) { + return array_keys($arr); +} + +/** + * @template T of int|string + * + * @param array $arr + * @return array + */ +function my_array_keys3($arr) { + return array_keys($arr); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2723.php b/tests/PHPStan/Rules/Functions/data/bug-2723.php new file mode 100644 index 0000000000..7aed24a439 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2723.php @@ -0,0 +1,66 @@ +t = $t; + } +} + +/** + * @template T2 + */ +class Bar +{ + /** @var T2 */ + public $t; + + /** @param T2 $t */ + public function __construct($t) + { + $this->t = $t; + } +} + +/** + * @template T3 + * @extends Bar> + */ +class BarOfFoo extends Bar +{ + /** @param T3 $t */ + public function __construct($t) + { + parent::__construct(new Foo($t)); + } +} + +/** + * @template T4 + * @param T4 $t + * @return Bar> + */ +function baz($t) +{ + return new BarOfFoo("hello"); +} + +/** + * @template T4 + * @param T4 $t + * @return Bar> + */ +function baz2($t) +{ + return new BarOfFoo($t); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2782.php b/tests/PHPStan/Rules/Functions/data/bug-2782.php new file mode 100644 index 0000000000..5f4f587a16 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2782.php @@ -0,0 +1,19 @@ + $j ? 1 : -1; + } + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-2846.php b/tests/PHPStan/Rules/Functions/data/bug-2846.php new file mode 100644 index 0000000000..5037a04418 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-2846.php @@ -0,0 +1,10 @@ + $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 new file mode 100644 index 0000000000..b5fc443ff0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3261.php @@ -0,0 +1,20 @@ + $a instanceof B + ); + +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-3566.php b/tests/PHPStan/Rules/Functions/data/bug-3566.php new file mode 100644 index 0000000000..6a015f9bfc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3566.php @@ -0,0 +1,40 @@ + $array + * @phpstan-param \Closure(TMemberType) : void $validator + */ + public static function validateArrayValueType(array $array, \Closure $validator) : void{ + foreach($array as $k => $v){ + try{ + $validator($v); + }catch(\TypeError $e){ + throw new \TypeError("Incorrect type of element at \"$k\": " . $e->getMessage(), 0, $e); + } + } + } + + /** + * @phpstan-template TMemberType + * @phpstan-param TMemberType $t + * @phpstan-param \Closure(int) : void $validator + */ + public static function doFoo($t, \Closure $validator) : void{ + $validator($t); + } + + /** + * @phpstan-template TMemberType + * @phpstan-param TMemberType $t + * @phpstan-param \Closure(mixed) : void $validator + */ + public static function doFoo2($t, \Closure $validator) : void{ + $validator($t); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3576.php b/tests/PHPStan/Rules/Functions/data/bug-3576.php new file mode 100644 index 0000000000..9f73084f22 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3576.php @@ -0,0 +1,45 @@ + $name + **/ + public function getConfig(string $name): void + { + call_user_func([$name, 'getArray']); + + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3631.php b/tests/PHPStan/Rules/Functions/data/bug-3631.php new file mode 100644 index 0000000000..e3cb0d28f6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3631.php @@ -0,0 +1,27 @@ + + */ +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 new file mode 100644 index 0000000000..6eb3bf468f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3660.php @@ -0,0 +1,9 @@ + strlen($str), $arr); + array_map(function($str) { return strlen($str);} ,$arr); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-3801.php b/tests/PHPStan/Rules/Functions/data/bug-3801.php new file mode 100644 index 0000000000..e0dba2bfc4 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3801.php @@ -0,0 +1,23 @@ +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-3891.php b/tests/PHPStan/Rules/Functions/data/bug-3891.php new file mode 100644 index 0000000000..7363317ded --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3891.php @@ -0,0 +1,43 @@ + + */ + public function sayHello(string $a) + { + + $arr = [ + 'a' => Two::class, + 'c' => Two::class, + ]; + return $arr[$a]; + } + + public function sayType(): void + { + call_user_func([One::class, 'isType']); + call_user_func([Two::class, 'isType']); + $class = $this->sayHello('a'); + $type = $class::isType(); + $callable = [$class, 'isType']; + call_user_func($callable); + if (is_callable($callable)) { + call_user_func($callable); + } + } +} + +class One { + public static function isType(): bool + { + return true; + } +} +class Two extends One { + +} +class Three {} 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 @@ + $date + */ +function takesDate(string $date): void {} + +function input(string $in): void { + switch ($in) { + case DateTime::class : + takesDate($in); + break; + case \stdClass::class : + takesDate($in); + break; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4455.php b/tests/PHPStan/Rules/Functions/data/bug-4455.php new file mode 100644 index 0000000000..76d3e774c7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4455.php @@ -0,0 +1,15 @@ + "green", "b" => "brown", "c" => "blue", "red"); +$array2 = array("a" => "GREEN", "B" => "brown", "yellow", "red"); + +array_uintersect($array1, $array2, "strcasecmp"); diff --git a/tests/PHPStan/Rules/Functions/data/bug-4612.php b/tests/PHPStan/Rules/Functions/data/bug-4612.php new file mode 100644 index 0000000000..3f8c3f6bd5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4612.php @@ -0,0 +1,16 @@ + $array */ +$array = []; + +foreach ($array as $k => $v) { + if (check($k) && isset($prev)) { + $array[$prev] = $v; + } + + $prev = $k; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4717.php b/tests/PHPStan/Rules/Functions/data/bug-4717.php new file mode 100644 index 0000000000..ba1a5dbd6c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4717.php @@ -0,0 +1,5 @@ + $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 new file mode 100644 index 0000000000..c5123a08d1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5356.php @@ -0,0 +1,24 @@ + 'a', $array['collectors']); + } + + public function doBar(): void + { + /** @var array{name: string, collectors: string[]} $array */ + $array = []; + + array_map(static function(array $_): string { return 'a'; }, $array['collectors']); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5592.php b/tests/PHPStan/Rules/Functions/data/bug-5592.php new file mode 100644 index 0000000000..56ae4204e5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5592.php @@ -0,0 +1,53 @@ + $map + * @return numeric-string + */ +function mapGet(\Ds\Map $map, \Ds\Hashable $key): string +{ + return $map->get($key, '0'); +} + +/** + * @template TDefault + * @param TDefault $default + * @return numeric-string|TDefault + */ +function getFooOrDefault($default) { + if ((bool) random_int(0, 1)) { + /** @var numeric-string */ + $foo = '5'; + return $foo; + } else { + return $default; + } +} + +function doStuff(): int +{ + /** + * @var \Ds\Map + */ + $map = new \Ds\Map(); + + return $map->get('foo', 1); +} + +/** + * @return numeric-string + */ +function doStuff1(): string { + /** @var numeric-string */ + $foo = '12'; + return getFooOrDefault($foo); +} + +/** + * @return numeric-string + */ +function doStuff2(): string { + return getFooOrDefault('12'); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5594.php b/tests/PHPStan/Rules/Functions/data/bug-5594.php new file mode 100644 index 0000000000..19dd58ed83 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5594.php @@ -0,0 +1,14 @@ + + */ +function createIterator(array $items): ArrayIterator +{ + return new ArrayIterator($items); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5609.php b/tests/PHPStan/Rules/Functions/data/bug-5609.php new file mode 100644 index 0000000000..503f4fe4fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5609.php @@ -0,0 +1,18 @@ +entities, function (\stdClass $std): bool { + return true; + }); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5661.php b/tests/PHPStan/Rules/Functions/data/bug-5661.php new file mode 100644 index 0000000000..471594e367 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5661.php @@ -0,0 +1,24 @@ + $array + */ + function sayHello(array $array): void + { + echo join(', ', $array) . PHP_EOL; + } + + /** + * @param string[] $array + */ + function sayHello2(array $array): void + { + echo join(', ', $array) . PHP_EOL; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5706.php b/tests/PHPStan/Rules/Functions/data/bug-5706.php new file mode 100644 index 0000000000..9255da5622 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5706.php @@ -0,0 +1,18 @@ +bar; + } +} + +class Foo +{ + public function getFoo(Bar $bar): void + { + $array = (array) $bar->getBar(); + $statusCode = array_key_exists('key', $array) ? (string) $array['key'] : null; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5844.php b/tests/PHPStan/Rules/Functions/data/bug-5844.php new file mode 100644 index 0000000000..29313fdd40 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5844.php @@ -0,0 +1,17 @@ +test(); diff --git a/tests/PHPStan/Rules/Functions/data/bug-5861.php b/tests/PHPStan/Rules/Functions/data/bug-5861.php new file mode 100644 index 0000000000..9f75e053b9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5861.php @@ -0,0 +1,10 @@ + + */ +class FooIterator implements \IteratorAggregate +{ + /** + * @return \Generator + */ + public function getIterator(): \Generator + { + yield 1; + yield 2; + yield 3; + } +} + +function (): void { + \array_map( + static function (int $i): string { + return (string) $i; + }, + \iterator_to_array(new FooIterator()) + ); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-5881.php b/tests/PHPStan/Rules/Functions/data/bug-5881.php new file mode 100644 index 0000000000..d586c9e246 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5881.php @@ -0,0 +1,11 @@ + */ + 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-6701.php b/tests/PHPStan/Rules/Functions/data/bug-6701.php new file mode 100644 index 0000000000..22370c0b92 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6701.php @@ -0,0 +1,27 @@ + $test ?? ''; + $b(null); + $b($i); + + $c = function ( ?string $test = null ): string { + return $test ?? ''; + }; + $c(null); + $c($i); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/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-first-class-callables.php b/tests/PHPStan/Rules/Functions/data/call-first-class-callables.php new file mode 100644 index 0000000000..4e78b9bb77 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-first-class-callables.php @@ -0,0 +1,30 @@ +doBar(...); + $f($mixed); + + $g = \Closure::fromCallable([$this, 'doBar']); + $g($mixed); + } + + /** + * @template T of object + * @param T $object + * @return T + */ + public function doBar($object) + { + return $object; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/call-generic-function.php b/tests/PHPStan/Rules/Functions/data/call-generic-function.php index f62deb6d3e..635348e59a 100644 --- a/tests/PHPStan/Rules/Functions/data/call-generic-function.php +++ b/tests/PHPStan/Rules/Functions/data/call-generic-function.php @@ -7,9 +7,9 @@ * @template B * @param int|array $a * @param int|array $b + * @return A[] */ -function f($a, $b): void { -} +function f($a, $b): array {} function test(): void { f(1, 2); @@ -18,10 +18,27 @@ function test(): void { /** * @template A of \DateTime * @param A $a + * @return A */ -function g($a): void { -} +function g($a) {} function testg(): void { g(new \DateTimeImmutable()); } + +/** + * @template TReturnType + * @param (callable(): TReturnType) $callback + * @return TReturnType + */ +function scope(callable $callback) { + return $callback(); +} + +function (): void { + scope( + function (): void { + throw new \Exception(); + } + ); +}; diff --git a/tests/PHPStan/Rules/Functions/data/call-to-define.php b/tests/PHPStan/Rules/Functions/data/call-to-define.php new file mode 100644 index 0000000000..fbb38e6306 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-to-define.php @@ -0,0 +1,6 @@ += 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-to-weird-functions.php b/tests/PHPStan/Rules/Functions/data/call-to-weird-functions.php index bcdbd46096..f826945416 100644 --- a/tests/PHPStan/Rules/Functions/data/call-to-weird-functions.php +++ b/tests/PHPStan/Rules/Functions/data/call-to-weird-functions.php @@ -11,8 +11,8 @@ strtok('/something', '/', 'foo'); // should report 3 parameters given, 1-2 required fputcsv($handle); fputcsv($handle, $data, ',', '""', '\\'); -/** @var resource $resource */ -$resource = imagecreatefrompng('filename'); + +$resource = imagecreatefrompng('filename'); if ($resource === false) { return; } imagepng(); // should report 1-4 parameters imagepng($resource); // OK imagepng($resource, 'to', 1, 2); // OK @@ -33,8 +33,10 @@ mysqli_fetch_all(new mysqli_result()); // OK mysqli_fetch_all(new mysqli_result(), MYSQLI_ASSOC); // OK mysqli_fetch_all(new mysqli_result(), MYSQLI_ASSOC, true); // should report 3 parameters given, 1-2 required -openssl_open('', $open, '', $resource); // OK -openssl_open('', $open, '', $resource, 'foo', 'bar', 'baz'); // should report 7 parameters, 4-6 required. +$keyId = openssl_pkey_get_private(''); +assert($keyId !== false); +openssl_open('', $open, '', $keyId); // OK +openssl_open('', $open, '', $keyId, 'foo', 'bar', 'baz'); // should report 7 parameters, 4-6 required. openssl_x509_parse('foo'); // OK openssl_x509_parse('foo', true); // OK diff --git a/tests/PHPStan/Rules/Functions/data/call-user-func-array.php b/tests/PHPStan/Rules/Functions/data/call-user-func-array.php new file mode 100644 index 0000000000..dd9f615517 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-user-func-array.php @@ -0,0 +1,3 @@ + ['bar' => 2]]); 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-named-arguments.php b/tests/PHPStan/Rules/Functions/data/callables-named-arguments.php new file mode 100644 index 0000000000..38e04e86c9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/callables-named-arguments.php @@ -0,0 +1,28 @@ += 8.0 + +namespace CallablesNullsafe; + +class Bar +{ + + public int $val; + +} + +function doFoo(?Bar $bar): void +{ + $fn = function (int $val) { + + }; + + $fn($bar?->val); +} + diff --git a/tests/PHPStan/Rules/Functions/data/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/closure-intersection-types.php b/tests/PHPStan/Rules/Functions/data/closure-intersection-types.php new file mode 100644 index 0000000000..90051d4342 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/closure-intersection-types.php @@ -0,0 +1,38 @@ += 8.1 + +namespace ClosureIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +function(Foo&Bar $a): Foo&Bar +{ + +}; + +function(Lorem&Ipsum $a): Lorem&Ipsum +{ + +}; + +function(int&mixed $a): int&mixed +{ + +}; diff --git a/tests/PHPStan/Rules/Functions/data/closure-uses-this.php b/tests/PHPStan/Rules/Functions/data/closure-uses-this.php deleted file mode 100644 index e298303d90..0000000000 --- a/tests/PHPStan/Rules/Functions/data/closure-uses-this.php +++ /dev/null @@ -1,26 +0,0 @@ - ? 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/create_function.php b/tests/PHPStan/Rules/Functions/data/create_function.php new file mode 100644 index 0000000000..8983c89301 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/create_function.php @@ -0,0 +1,5 @@ + '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 @@ += 8.1 + +namespace FirstClassCallableFunctionWithoutSideEffect; + +class Foo +{ + + public static function doFoo(): void + { + $f = mkdir(...); + + mkdir(...); + } + +} + +class Bar +{ + + public static function doFoo(): void + { + $f = strlen(...); + + strlen(...); + } + +} + +function foo(): never +{ + throw new \Exception(); +} + +function (): void { + $f = foo(...); + foo(...); +}; + +/** + * @throws \Exception + */ +function bar() +{ + throw new \Exception(); +} + +function (): void { + $f = bar(...); + bar(...); +}; diff --git a/tests/PHPStan/Rules/Functions/data/first-class-callables.php b/tests/PHPStan/Rules/Functions/data/first-class-callables.php new file mode 100644 index 0000000000..da237d1130 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/first-class-callables.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FirstClassFunctionCallable; + +class Foo +{ + + public function doFoo(): void + { + $f = json_encode(...); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/flock.php b/tests/PHPStan/Rules/Functions/data/flock.php new file mode 100644 index 0000000000..2140be6711 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/flock.php @@ -0,0 +1,47 @@ + "bar",]); + + fputcsv($handle, [new Person,]); // Problem on this line + + // This is valid. PersonWithToString should be cast to string by fputcsv + fputcsv($handle, [new PersonWithToString()]); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/function-attributes.php b/tests/PHPStan/Rules/Functions/data/function-attributes.php new file mode 100644 index 0000000000..5ed22c10fe --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-attributes.php @@ -0,0 +1,39 @@ + 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) + { + \PHPStan\Testing\assertType('string', $s); + \PHPStan\Testing\assertNativeType('string', $s); + \PHPStan\Testing\assertVariableCertainty(TrinaryLogic::createYes(), $s); } } diff --git a/tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php b/tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php new file mode 100644 index 0000000000..534baa8174 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FunctionCallableNotSupported; + +class Foo +{ + + public function doFoo(): void + { + $f = json_encode(...); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/function-callable.php b/tests/PHPStan/Rules/Functions/data/function-callable.php new file mode 100644 index 0000000000..8fe1d46880 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-callable.php @@ -0,0 +1,55 @@ += 8.1 + +namespace FunctionCallable; + +use function function_exists; + +class Foo +{ + + public function doFoo(string $s): void + { + strlen(...); + nonexistent(...); + + if (function_exists('blabla')) { + blabla(...); + } + + $s(...); + if (function_exists($s)) { + $s(...); + } + } + + public function doBar(): void + { + $f = function (): void { + + }; + $f(...); + + $i = 1; + $i(...); + } + + public function doBaz(): void + { + StrLen(...); + } + + public function doLorem(callable $cb): void + { + if (rand(0, 1)) { + $cb = 1; + } + + $f = $cb(...); + } + + public function doIpsum(Nonexistent $obj): void + { + $f = $obj(...); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/implode-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/implode.php b/tests/PHPStan/Rules/Functions/data/implode.php new file mode 100644 index 0000000000..800a61397d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode.php @@ -0,0 +1,26 @@ + '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 @@ += 8.1 + +namespace FunctionIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +function doFoo(Foo&Bar $a): Foo&Bar +{ + +} + +function doBar(Lorem&Ipsum $a): Lorem&Ipsum +{ + +} + +function doBaz(int&mixed $a): int&mixed +{ + +} diff --git a/tests/PHPStan/Rules/Functions/data/invalid-lexical-variables-in-closure-use.php b/tests/PHPStan/Rules/Functions/data/invalid-lexical-variables-in-closure-use.php new file mode 100644 index 0000000000..310241c852 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/invalid-lexical-variables-in-closure-use.php @@ -0,0 +1,118 @@ +foo())(); + }; + } + + /** + * @return \Closure(): array + */ + public function superglobals(): \Closure + { + return function () use ($GLOBALS, $_COOKIE, $_ENV, $_FILES, $_GET, $_POST, $_REQUEST, $_SERVER, $_SESSION): array { + return array_merge( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION, + ); + }; + } + + /** + * @return \Closure(int, string, bool): bool + */ + public function sameAsParameter(): \Closure + { + return function (int $foo, string $bar, bool $baz) use ($baz): bool { + return $baz; + }; + } + + /** + * @return \Closure(): string + */ + public function multilineThis(): \Closure + { + $message = 'hello'; + + return function () use ( + $this, + $message + ): string { + return ($this->foo())() . $message; + }; + } + + /** + * @return \Closure(): array + */ + public function multilineSuperglobals(): \Closure + { + return function () use ( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION + ): array { + return array_merge( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION, + ); + }; + } + + /** + * @return \Closure(int, string, bool): bool + */ + public function multilineSameAsParameter(): \Closure + { + return function (int $foo, string $bar, bool $baz) use ( + $baz, + $bar, + ): bool { + return (bool) ($baz . $bar); + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/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 @@ += 8.0 + +namespace MatchExprAnalysis; + +class Foo +{ + + public function doFoo() + { + match (lorem()) { + ipsum() => dolor(), + default => sit(), + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/mb_ereg_replace_callback.php b/tests/PHPStan/Rules/Functions/data/mb_ereg_replace_callback.php new file mode 100644 index 0000000000..40c10a42ac --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/mb_ereg_replace_callback.php @@ -0,0 +1,50 @@ + $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/missing-function-return-typehint.php b/tests/PHPStan/Rules/Functions/data/missing-function-return-typehint.php index 66c5f982ea..6ab9bd061c 100644 --- a/tests/PHPStan/Rules/Functions/data/missing-function-return-typehint.php +++ b/tests/PHPStan/Rules/Functions/data/missing-function-return-typehint.php @@ -90,4 +90,62 @@ function returnsGenericClass(): GenericClass { } + + /** + * @return GenericClass, GenericClass> + */ + function genericGenericValidArgs(): GenericClass + { + + } + + /** + * @return GenericClass + */ + function genericGenericMissingTemplateArgs(): GenericClass + { + + } + + /** + * @return \Closure + */ + function closureWithNoPrototype() : \Closure{ + + } + + /** + * @return \Closure(int) : void + */ + function closureWithPrototype() : \Closure{ + + } + + /** + * @return callable + */ + function callableWithNoPrototype() : callable{ + + } + + /** + * @return callable(int) : void + */ + function callableWithPrototype() : callable{ + + } + + /** + * @return callable(callable) : void + */ + function callableNestedNoPrototype() : callable{ + + } + + /** + * @return callable(callable(int) : void) : void + */ + function callableNestedWithPrototype() : callable{ + + } } 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/named-arguments-define.php b/tests/PHPStan/Rules/Functions/data/named-arguments-define.php new file mode 100644 index 0000000000..279ca1e656 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-define.php @@ -0,0 +1,13 @@ + ['b', 'c']]); // works - userland + array_merge(...['a' => ['b', 'c']]); // doesn't work - internal +} diff --git a/tests/PHPStan/Rules/Functions/data/native-union-types.php b/tests/PHPStan/Rules/Functions/data/native-union-types.php new file mode 100644 index 0000000000..9f8c46574d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/native-union-types.php @@ -0,0 +1,24 @@ += 8.0 + +namespace NativeUnionTypesSupport; + +function foo(int|bool $foo): int|bool +{ + return 1; +} + +function bar(): int|bool +{ + +} + +function (int|bool $foo): int|bool { + +}; + +function (): int|bool { + +}; + +fn (int|bool $foo): int|bool => 1; +fn (): int|bool => 1; 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/number-format-named-arguments.php b/tests/PHPStan/Rules/Functions/data/number-format-named-arguments.php new file mode 100644 index 0000000000..fea39c2ad1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/number-format-named-arguments.php @@ -0,0 +1,5 @@ += 8.0 + +\number_format(200.00, decimals: 2, decimal_separator: ',', thousands_separator: ' '); + +$name = implode(separator: ' ', array: ['John', 'Doe']); diff --git a/tests/PHPStan/Rules/Functions/data/param-attributes.php b/tests/PHPStan/Rules/Functions/data/param-attributes.php new file mode 100644 index 0000000000..39231f293e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-attributes.php @@ -0,0 +1,89 @@ += 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 @@ + 'bogus', 'in' => 'here'], [], $pipes); diff --git a/tests/PHPStan/Rules/Functions/data/redefined-parameters.php b/tests/PHPStan/Rules/Functions/data/redefined-parameters.php new file mode 100644 index 0000000000..2ce75225c2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/redefined-parameters.php @@ -0,0 +1,32 @@ + (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 @@ += 8.0 + +namespace RequiredAfterOptional; + +fn ($foo = null, $bar): int => 1; // not OK + +fn (int $foo = null, $bar): int => 1; // is OK + +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 new file mode 100644 index 0000000000..da96ec6909 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php @@ -0,0 +1,47 @@ += 8.0 + +namespace RequiredAfterOptional; + +function ($foo = null, $bar): void // not OK +{ +}; + +function (int $foo = null, $bar): void // is OK +{ +}; + +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 new file mode 100644 index 0000000000..8937d262a7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php @@ -0,0 +1,52 @@ += 8.0 + +namespace RequiredAfterOptional; + +function doFoo($foo = null, $bar): void // not OK +{ + +} + +function doBar(int $foo = null, $bar): void // is OK +{ +} + +function doBaz(int $foo = 1, $bar): void // not OK +{ +} + +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/return-null-safe-by-ref.php b/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref.php new file mode 100644 index 0000000000..52f06c86d6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/return-null-safe-by-ref.php @@ -0,0 +1,37 @@ +bar->foo; + }; + } + + public function &doBar() + { + if (rand(0, 1)) { + return $foo; + } + + return $foo?->bar->foo; + } + +} + +function &foo(): void +{ + if (rand(0, 1)) { + return $foo; + } + + return $foo?->bar->foo; +} diff --git a/tests/PHPStan/Rules/Functions/data/returnTypes.php b/tests/PHPStan/Rules/Functions/data/returnTypes.php index cf5527e70a..3e15651c69 100644 --- a/tests/PHPStan/Rules/Functions/data/returnTypes.php +++ b/tests/PHPStan/Rules/Functions/data/returnTypes.php @@ -172,3 +172,11 @@ function returnVoidFromGenerator2(): \Generator yield 1; return 2; } + +/** + * @return never + */ +function returnNever() +{ + return; +} 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/typehints.php b/tests/PHPStan/Rules/Functions/data/typehints.php index e4395b7ed9..67ca9b736c 100644 --- a/tests/PHPStan/Rules/Functions/data/typehints.php +++ b/tests/PHPStan/Rules/Functions/data/typehints.php @@ -88,3 +88,12 @@ function genericTemplateClassString(string $string) { } + +/** + * @template T + * @param class-string $a + */ +function templateTypeMissingInParameter(string $a) +{ + +} diff --git a/tests/PHPStan/Rules/Functions/data/uasort.php b/tests/PHPStan/Rules/Functions/data/uasort.php new file mode 100644 index 0000000000..8b34c003f4 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/uasort.php @@ -0,0 +1,10 @@ + 1 +); diff --git a/tests/PHPStan/Rules/Functions/data/uksort.php b/tests/PHPStan/Rules/Functions/data/uksort.php new file mode 100644 index 0000000000..2d289b58eb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/uksort.php @@ -0,0 +1,55 @@ + 1, 'two' => 2, 'three' => 3]; + + uksort( + $array, + function (\stdClass $one, \stdClass $two): int { + return 1; + } + ); + } + +} + +class Bar +{ + + /** @var Foo[] */ + private $unknownKeys; + + /** @var array */ + private $stringKeys; + + /** @var array */ + private $intKeys; + + public function doFoo(): void + { + uksort($this->unknownKeys, function (string $one, string $two) { + return 1; + }); + } + + public function doFoo2(): void + { + uksort($this->stringKeys, function (string $one, string $two) { + return 1; + }); + } + + public function doFoo3(): void + { + uksort($this->intKeys, function (string $one, string $two) { + return 1; + }); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/uksort_arrow.php b/tests/PHPStan/Rules/Functions/data/uksort_arrow.php new file mode 100644 index 0000000000..4de3c07029 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/uksort_arrow.php @@ -0,0 +1,47 @@ + 1, 'two' => 2, 'three' => 3]; + + uksort( + $array, + fn (\stdClass $one, \stdClass $two): int => 1 + ); + } + +} + +class Bar +{ + + /** @var Foo[] */ + private $unknownKeys; + + /** @var array */ + private $stringKeys; + + /** @var array */ + private $intKeys; + + public function doFoo(): void + { + uksort($this->unknownKeys, fn (string $one, string $two) => 1); + } + + public function doFoo2(): void + { + uksort($this->stringKeys, fn (string $one, string $two) => 1); + } + + public function doFoo3(): void + { + uksort($this->intKeys, fn (string $one, string $two) => 1); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/useless-fn-return-php8.php b/tests/PHPStan/Rules/Functions/data/useless-fn-return-php8.php new file mode 100644 index 0000000000..2114e7152a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/useless-fn-return-php8.php @@ -0,0 +1,24 @@ += 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.php b/tests/PHPStan/Rules/Functions/data/usort.php new file mode 100644 index 0000000000..f17f3c8c89 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/usort.php @@ -0,0 +1,20 @@ + 1 + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/variadic-parameters-declaration.php b/tests/PHPStan/Rules/Functions/data/variadic-parameters-declaration.php new file mode 100644 index 0000000000..cc5c195b16 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/variadic-parameters-declaration.php @@ -0,0 +1,23 @@ +format('j. n. Y'); + } + + public function variadicParamAtEnd(int $number, int ...$numbers): void + { + } +} + +function variadicFunction(int ...$a, string $b): void +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/varying-acceptor.php b/tests/PHPStan/Rules/Functions/data/varying-acceptor.php new file mode 100644 index 0000000000..e98efb1082 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/varying-acceptor.php @@ -0,0 +1,19 @@ + + * @extends RuleTestCase */ class YieldFromTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new YieldFromTypeRule(new RuleLevelHelper($this->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 @@ -37,10 +37,20 @@ public function testRule(): void 41, ], [ - 'Generator expects value type array(DateTime, DateTime, stdClass, DateTimeImmutable), array(0 => DateTime, 1 => DateTime, 2 => stdClass, 4 => DateTimeImmutable) given.', + 'Generator expects value type array{DateTime, DateTime, stdClass, DateTimeImmutable}, array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable} given.', 74, + 'Array does not have offset 3.', + ], + [ + 'Result of yield from (void) is used.', + 111, ], ]); } + public function testBug11517(): void + { + $this->analyse([__DIR__ . '/data/bug-11517.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php index cb10501536..7e234250d8 100644 --- a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class YieldInGeneratorRuleTest extends RuleTestCase { @@ -51,6 +51,14 @@ public function testRule(): void 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', 56, ], + [ + 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', + 87, + ], + [ + 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', + 88, + ], ]); } diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index ec516b3c9d..500199d28e 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -7,14 +7,14 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ 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 @@ -45,8 +45,28 @@ public function testRule(): void 17, ], [ - 'Generator expects value type array(0 => DateTime, 1 => DateTime, 2 => stdClass, 4 => DateTimeImmutable), array(DateTime, DateTime, stdClass, DateTimeImmutable) given.', + 'Generator expects value type array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable}, array{DateTime, DateTime, stdClass, DateTimeImmutable} given.', 25, + 'Array does not have offset 4.', + ], + [ + 'Result of yield (void) is used.', + 137, + ], + [ + 'Result of yield (void) is used.', + 138, + ], + ]); + } + + 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-from.php b/tests/PHPStan/Rules/Generators/data/yield-from.php index b6299ea11c..1bab7f130f 100644 --- a/tests/PHPStan/Rules/Generators/data/yield-from.php +++ b/tests/PHPStan/Rules/Generators/data/yield-from.php @@ -74,4 +74,40 @@ public function doArrayShape2(): \Generator yield from $this->doArrayShape(); } + /** + * @return \Generator + */ + public function yieldWithImplicitReturn() : \Generator{ + yield 1; + return 1; + } + + /** + * @return \Generator + */ + public function yieldWithExplicitReturn() : \Generator{ + yield 1; + return 1; + } + + /** + * @return \Generator + */ + public function yieldWithVoidReturn() : \Generator{ + yield 1; + } + + /** + * @return \Generator + */ + public function yieldFromResult() : \Generator{ + yield from $this->yieldWithImplicitReturn(); + $mixed = yield from $this->yieldWithImplicitReturn(); + + yield from $this->yieldWithExplicitReturn(); + $int = yield from $this->yieldWithExplicitReturn(); + + yield from $this->yieldWithVoidReturn(); + $void = yield from $this->yieldWithVoidReturn(); + } } diff --git a/tests/PHPStan/Rules/Generators/data/yield-in-generator.php b/tests/PHPStan/Rules/Generators/data/yield-in-generator.php index 90f3fd71cf..d896ab4986 100644 --- a/tests/PHPStan/Rules/Generators/data/yield-in-generator.php +++ b/tests/PHPStan/Rules/Generators/data/yield-in-generator.php @@ -78,3 +78,12 @@ function doFooBar() { yield 'test'; } + +/** + * @return never + */ +function doNever() +{ + yield 1; + yield from doFoo(); +} diff --git a/tests/PHPStan/Rules/Generators/data/yield.php b/tests/PHPStan/Rules/Generators/data/yield.php index e1c6d5b016..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 { @@ -119,3 +119,30 @@ function hash_to_alt_sequence2(iterable $hash): iterable } } + +/** + * @return \Generator + */ +function yieldWithIntSendType(){ + yield 1; + $something = yield 1; + var_dump(yield 1); +} + +/** + * @return \Generator + */ +function yieldWithVoidSendType(){ + yield 1; + $something = yield 1; + var_dump(yield 1); +} + +/** + * @return \Generator + */ +function yieldWithoutSendType(){ + yield 1; + $something = yield 1; + var_dump(yield 1); +} diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index 30aa7caa88..dd82f5bada 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -2,12 +2,12 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ClassAncestorsRuleTest extends RuleTestCase { @@ -15,13 +15,15 @@ class ClassAncestorsRuleTest extends RuleTestCase protected function getRule(): Rule { return new ClassAncestorsRule( - self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true - ) + 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,12 +89,47 @@ 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', + 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, + ], ]); } @@ -116,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.', @@ -130,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', @@ -175,12 +207,81 @@ 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, + ], + ]); + } + + public function testBug3922(): void + { + $this->analyse([__DIR__ . '/data/bug-3922-ancestors.php'], [ + [ + 'Type Bug3922Ancestors\BarQuery in generic type Bug3922Ancestors\QueryHandlerInterface in PHPDoc tag @implements is not subtype of template type TQuery of Bug3922Ancestors\QueryInterface of interface Bug3922Ancestors\QueryHandlerInterface.', + 54, + ], + ]); + } + + public function testBug3922Reversed(): void + { + $this->analyse([__DIR__ . '/data/bug-3922-ancestors-reversed.php'], [ + [ + 'Type string in generic type Bug3922AncestorsReversed\QueryHandlerInterface in PHPDoc tag @implements is not subtype of template type int of interface Bug3922AncestorsReversed\QueryHandlerInterface.', + 54, + ], + ]); + } + + public function testCrossCheckInterfaces(): void + { + $this->analyse([__DIR__ . '/data/cross-check-interfaces.php'], [ + [ + 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfaces\Item.', + 19, + ], + ]); + } + + public function testScalarClassName(): void + { + $this->analyse([__DIR__ . '/data/scalar-class-name.php'], []); + } + + 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 b17ce01b1d..0ef409ddf8 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -3,23 +3,36 @@ 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; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ClassTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new ClassTemplateTypeRule( - self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -35,16 +48,106 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for class ClassTemplateType\Baz with bound type int is not supported.', - 24, + 'Class ClassTemplateType\Baz referenced with incorrect case: ClassTemplateType\baz.', + 32, + ], + [ + 'PHPDoc tag @template for class ClassTemplateType\Ipsum cannot have existing type alias TypeAlias as its name.', + 41, + ], + [ + 'PHPDoc tag @template for class ClassTemplateType\Dolor cannot have existing type alias LocalAlias as its name.', + 53, + ], + [ + 'PHPDoc tag @template for class ClassTemplateType\Dolor cannot have existing type alias ImportedAlias as its name.', + 53, + ], + [ + 'PHPDoc tag @template for anonymous class cannot have existing class stdClass as its name.', + 58, + ], + [ + 'PHPDoc tag @template T for anonymous class has invalid bound type ClassTemplateType\Zazzzu.', + 63, ], [ 'Class ClassTemplateType\Baz referenced with incorrect case: ClassTemplateType\baz.', + 73, + ], + [ + '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, + ], + ]); + } + + public function testNestedGenericTypes(): void + { + $this->analyse([__DIR__ . '/data/nested-generic-types.php'], [ + [ + 'Type mixed in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of interface NestedGenericTypesClassCheck\SomeObjectInterface.', 32, ], [ - 'PHPDoc tag @template for class ClassTemplateType\Ipsum cannot have existing type alias TypeAlias as its name.', - 40, + 'Type int in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of interface NestedGenericTypesClassCheck\SomeObjectInterface.', + 41, + ], + [ + 'PHPDoc tag @template U bound contains generic type NestedGenericTypesClassCheck\NotGeneric but interface NestedGenericTypesClassCheck\NotGeneric is not generic.', + 52, + ], + [ + 'PHPDoc tag @template V bound has type NestedGenericTypesClassCheck\MultipleGenerics which does not specify all template types of interface NestedGenericTypesClassCheck\MultipleGenerics: T, U', + 52, + ], + [ + 'PHPDoc tag @template W bound has type NestedGenericTypesClassCheck\MultipleGenerics which specifies 3 template types, but interface NestedGenericTypesClassCheck\MultipleGenerics supports only 2: T, U', + 52, + ], + ]); + } + + public function testBug5446(): void + { + $this->analyse([__DIR__ . '/data/bug-5446.php'], []); + } + + public function testInInterface(): void + { + $this->analyse([__DIR__ . '/data/interface-template.php'], []); + } + + 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 new file mode 100644 index 0000000000..18181f9bae --- /dev/null +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -0,0 +1,79 @@ + + */ +class EnumAncestorsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumAncestorsRule( + new GenericAncestorsCheck( + $this->createReflectionProvider(), + new GenericObjectTypeCheck(), + new VarianceCheck(), + new UnresolvableTypeHelper(), + [], + true, + ), + new CrossCheckInterfacesHelper(), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-ancestors.php'], [ + [ + 'Enum EnumGenericAncestors\Foo has @implements tag, but does not implement any interface.', + 22, + ], + [ + 'PHPDoc tag @implements contains generic type EnumGenericAncestors\NonGeneric but interface EnumGenericAncestors\NonGeneric is not generic.', + 35, + ], + [ + 'Enum EnumGenericAncestors\Foo4 implements generic interface EnumGenericAncestors\Generic but does not specify its types: T, U', + 40, + ], + [ + 'Generic type EnumGenericAncestors\Generic in PHPDoc tag @implements does not specify all template types of interface EnumGenericAncestors\Generic: T, U', + 56, + ], + [ + 'Enum EnumGenericAncestors\Foo7 has @extends tag, but cannot extend anything.', + 64, + ], + [ + 'Call-site variance annotation of covariant EnumGenericAncestors\NonGeneric in generic type EnumGenericAncestors\Generic in PHPDoc tag @implements is not allowed.', + 93, + ], + ]); + } + + public function testCrossCheckInterfaces(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/cross-check-interfaces-enums.php'], [ + [ + 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfacesEnums\Item.', + 19, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php new file mode 100644 index 0000000000..dbd82cf985 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php @@ -0,0 +1,38 @@ + + */ +class EnumTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumTemplateTypeRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-template.php'], [ + [ + 'Enum EnumTemplate\Foo has PHPDoc @template tag but enums cannot be generic.', + 8, + ], + [ + 'Enum EnumTemplate\Bar has PHPDoc @template tags but enums cannot be generic.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php index 42008742b0..f80773321a 100644 --- a/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class FunctionSignatureVarianceRuleTest extends RuleTestCase { @@ -14,7 +14,7 @@ class FunctionSignatureVarianceRuleTest extends RuleTestCase protected function getRule(): Rule { return new FunctionSignatureVarianceRule( - self::getContainer()->getByType(VarianceCheck::class) + self::getContainer()->getByType(VarianceCheck::class), ); } @@ -22,15 +22,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/function-signature-variance.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter a of function FunctionSignatureVariance\f().', - 20, - ], - [ - 'Template type T is declared as covariant, but occurs in invariant position in parameter b of function FunctionSignatureVariance\f().', - 20, - ], - [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter c of function FunctionSignatureVariance\f().', + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type T in in function FunctionSignatureVariance\f().', 20, ], ]); diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 61d290b945..af46e7e0f5 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -3,22 +3,37 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class FunctionTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -33,15 +48,46 @@ public function testRule(): void 'PHPDoc tag @template T for function FunctionTemplateType\bar() has invalid bound type FunctionTemplateType\Zazzzu.', 16, ], - [ - 'PHPDoc tag @template T for function FunctionTemplateType\baz() with bound type int is not supported.', - 24, - ], [ 'PHPDoc tag @template for function FunctionTemplateType\lorem() cannot have existing type alias TypeAlias as its name.', 32, ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\resourceBound() with bound type resource is not supported.', + 50, + ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\nullNotSupported() with bound type null is not supported.', + 68, + ], + [ + '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, + ], ]); } + public function testBug3769(): void + { + require_once __DIR__ . '/data/bug-3769.php'; + $this->analyse([__DIR__ . '/data/bug-3769.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php index 6ffca19004..f82610ea0b 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php @@ -2,12 +2,12 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class InterfaceAncestorsRuleTest extends RuleTestCase { @@ -15,13 +15,15 @@ class InterfaceAncestorsRuleTest extends RuleTestCase protected function getRule(): Rule { return new InterfaceAncestorsRule( - self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true - ) + 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,25 @@ 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, + ], + ]); + } + + public function testCrossCheckInterfaces(): void + { + $this->analyse([__DIR__ . '/data/cross-check-interfaces-interfaces.php'], [ + [ + 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfacesInInterfaces\Item.', + 19, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 4822752adf..3823a214f7 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -3,22 +3,35 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class InterfaceTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new InterfaceTemplateTypeRule( - self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -34,14 +47,44 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for interface InterfaceTemplateType\Baz with bound type int is not supported.', - 24, + 'PHPDoc tag @template for interface InterfaceTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', + 33, ], [ - 'PHPDoc tag @template for interface InterfaceTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', - 32, + 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias LocalAlias as its name.', + 45, + ], + [ + '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, ], ]); } + public function testInClass(): void + { + $this->analyse([__DIR__ . '/data/class-template.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index a06eec51e9..ec893f0947 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MethodSignatureVarianceRuleTest extends RuleTestCase { @@ -14,7 +15,7 @@ class MethodSignatureVarianceRuleTest extends RuleTestCase protected function getRule(): Rule { return new MethodSignatureVarianceRule( - self::getContainer()->getByType(VarianceCheck::class) + self::getContainer()->getByType(VarianceCheck::class), ); } @@ -22,28 +23,228 @@ 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().', - 23, + '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().', - 23, + '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 X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + '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 T is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\C::a().', - 23, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\StaticMethod\B::a().', + 43, ], [ - 'Template type U is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\C::b().', - 33, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\StaticMethod\B::a().', + 43, ], [ - 'Template type U is declared as covariant, but occurs in invariant position in parameter b of method MethodSignatureVariance\C::b().', - 33, + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\StaticMethod\B::c().', + 49, ], [ - 'Template type U is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\C::b().', - 33, + '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 0bf8add7fc..8450ba5f3e 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -3,27 +3,44 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MethodTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } public function testRule(): void { + require_once __DIR__ . '/data/method-template.php'; + $this->analyse([__DIR__ . '/data/method-template.php'], [ [ 'PHPDoc tag @template for method MethodTemplateType\Foo::doFoo() cannot have existing class stdClass as its name.', @@ -38,12 +55,37 @@ public function testRule(): void 37, ], [ - 'PHPDoc tag @template T for method MethodTemplateType\Baz::doFoo() with bound type int is not supported.', - 50, + 'PHPDoc tag @template for method MethodTemplateType\Lorem::doFoo() cannot have existing type alias TypeAlias as its name.', + 66, ], [ - 'PHPDoc tag @template for method MethodTemplateType\Lorem::doFoo() cannot have existing type alias TypeAlias as its name.', - 63, + 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias LocalAlias as its name.', + 85, + ], + [ + '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 26b6b61e5c..335e1f707c 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -3,27 +3,44 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TraitTemplateTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), ['TypeAlias' => 'int'], true) + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } public function testRule(): void { + require_once __DIR__ . '/data/trait-template.php'; + $this->analyse([__DIR__ . '/data/trait-template.php'], [ [ 'PHPDoc tag @template for trait TraitTemplateType\Foo cannot have existing class stdClass as its name.', @@ -34,12 +51,37 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for trait TraitTemplateType\Baz with bound type int is not supported.', - 24, + 'PHPDoc tag @template for trait TraitTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', + 33, ], [ - 'PHPDoc tag @template for trait TraitTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', - 32, + 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias LocalAlias as its name.', + 45, + ], + [ + '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 new file mode 100644 index 0000000000..76dc75145f --- /dev/null +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -0,0 +1,65 @@ + + */ +class UsedTraitsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UsedTraitsRule( + self::getContainer()->getByType(FileTypeMapper::class), + new GenericAncestorsCheck( + $this->createReflectionProvider(), + new GenericObjectTypeCheck(), + new VarianceCheck(), + new UnresolvableTypeHelper(), + [], + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/used-traits.php'], [ + [ + 'PHPDoc tag @use contains generic type UsedTraits\NongenericTrait but trait UsedTraits\NongenericTrait is not generic.', + 20, + ], + [ + 'Type int in generic type UsedTraits\GenericTrait in PHPDoc tag @use is not subtype of template type T of object of trait UsedTraits\GenericTrait.', + 31, + ], + [ + 'Class UsedTraits\Baz uses generic trait UsedTraits\GenericTrait but does not specify its types: T', + 38, + ], + [ + 'Generic type UsedTraits\GenericTrait in PHPDoc tag @use specifies 2 template types, but trait UsedTraits\GenericTrait supports only 1: T', + 46, + ], + [ + 'The @use tag of trait UsedTraits\NestedTrait describes UsedTraits\NongenericTrait but the trait uses UsedTraits\GenericTrait.', + 54, + ], + [ + 'Trait UsedTraits\NestedTrait uses generic trait UsedTraits\GenericTrait but does not specify its types: T', + 54, + ], + [ + '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 new file mode 100644 index 0000000000..aeb273d479 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -0,0 +1,112 @@ + $in + * @return array + */ +function stringValues(array $in): array { + $a = assertType('array', $in); + return array_map(function (int $int): string { + return (string) $int; + }, $in); +} + +/** + * @param array $foo + * @param array $bar + * @param array $baz + */ +function foo( + array $foo, + array $bar, + array $baz +): void { + $a = assertType('array', stringValues($foo)); + $a = assertType('array', stringValues($bar)); + $a = assertType('array', stringValues($baz)); + echo 'test'; +}; + +/** + * @template T of \stdClass|\Exception + * @param T $foo + */ +function fooUnion($foo): void { + $a = assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo); + echo 'test'; +} + +/** + * @template T + * @param T $a + * @return T + */ +function mixedBound($a) +{ + return $a; +} + +/** + * @template T of int + * @param T $a + * @return T + */ +function intBound(int $a) +{ + return $a; +} + +/** + * @template T of string + * @param T $a + * @return T + */ +function stringBound(string $a) +{ + return $a; +} + +function (): void { + $a = assertType('1', mixedBound(1)); + $a = assertType('\'str\'', mixedBound('str')); + $a = assertType('1', intBound(1)); + $a = assertType('\'str\'', stringBound('str')); +}; + +/** @template T of string */ +class Foo +{ + + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function getValue() + { + return $this->value; + } + +} + +/** @param Foo<'bar'> $foo */ +function testTofString(Foo $foo): void { + $a = assertType('\'bar\'', $foo->getValue()); + + $baz = new Foo('baz'); + $a = assertType('\'baz\'', $baz->getValue()); +}; diff --git a/tests/PHPStan/Rules/Generics/data/bug-3922-ancestors-reversed.php b/tests/PHPStan/Rules/Generics/data/bug-3922-ancestors-reversed.php new file mode 100644 index 0000000000..acd2fbd6da --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-3922-ancestors-reversed.php @@ -0,0 +1,60 @@ + + * @template TResult + */ +interface QueryHandlerInterface +{ + /** + * @param TQuery $query + * + * @return TResult + */ + public function handle(QueryInterface $query); +} + +/** + * @template TResult + */ +interface QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class FooQuery implements QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class BarQuery implements QueryInterface +{ +} + +/** + * @template-implements QueryHandlerInterface + */ +final class FooQueryHandler implements QueryHandlerInterface +{ + public function handle(QueryInterface $query): string + { + return 'foo'; + } +} + +/** + * @template-implements QueryHandlerInterface + */ +final class BarQueryHandler implements QueryHandlerInterface +{ + public function handle(QueryInterface $query): int + { + return 10; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-3922-ancestors.php b/tests/PHPStan/Rules/Generics/data/bug-3922-ancestors.php new file mode 100644 index 0000000000..90a960e388 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-3922-ancestors.php @@ -0,0 +1,60 @@ + + */ +interface QueryHandlerInterface +{ + /** + * @param TQuery $query + * + * @return TResult + */ + public function handle(QueryInterface $query); +} + +/** + * @template TResult + */ +interface QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class FooQuery implements QueryInterface +{ +} + +/** + * @template-implements QueryInterface + */ +final class BarQuery implements QueryInterface +{ +} + +/** + * @template-implements QueryHandlerInterface + */ +final class FooQueryHandler implements QueryHandlerInterface +{ + public function handle(QueryInterface $query): string + { + return 'foo'; + } +} + +/** + * @template-implements QueryHandlerInterface + */ +final class BarQueryHandler implements QueryHandlerInterface +{ + public function handle(QueryInterface $query): int + { + return 10; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-5446.php b/tests/PHPStan/Rules/Generics/data/bug-5446.php new file mode 100644 index 0000000000..a059178a71 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-5446.php @@ -0,0 +1,25 @@ + + * @template Fourth of \Bug5446\D + */ +class X {} diff --git a/tests/PHPStan/Rules/Generics/data/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 62a1cd9303..c04a5665a4 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -193,3 +193,83 @@ 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 +{ + + public function accept() + { + return true; + } + +} + +/** @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 10adfa2f0f..06400bd536 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -19,7 +19,7 @@ class Bar } /** - * @template T of int + * @template T of float */ class Baz { @@ -35,9 +35,108 @@ class Lorem } /** + * @phpstan-type ExportedAlias string * @template TypeAlias */ class Ipsum { } + +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Ipsum as ImportedAlias + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ +class Dolor +{ + +} + +new /** @template stdClass */ class +{ + +}; + +new /** @template T of Zazzzu */ class +{ + +}; + +new /** @template T of float */ class +{ + +}; + +new /** @template T of baz */ class +{ + +}; + +new /** @template TypeAlias */ class +{ + +}; + +/** + * @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/cross-check-interfaces-enums.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-enums.php new file mode 100644 index 0000000000..d0dac4e481 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-enums.php @@ -0,0 +1,36 @@ += 8.1 + +namespace CrossCheckInterfacesEnums; + +final class Item +{ +} + +/** + * @extends \Traversable + */ +interface ItemListInterface extends \Traversable +{ +} + +/** + * @implements \IteratorAggregate + */ +enum ItemList implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} + +/** + * @implements \IteratorAggregate + */ +enum ItemList2 implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} diff --git a/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-interfaces.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-interfaces.php new file mode 100644 index 0000000000..8900cefd5c --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-interfaces.php @@ -0,0 +1,48 @@ + + */ +interface ItemListInterface extends \Traversable +{ +} + +/** + * @extends \IteratorAggregate + */ +interface ItemList extends \IteratorAggregate, ItemListInterface +{ + +} + +/** + * @extends \IteratorAggregate + */ +interface ItemList2 extends \IteratorAggregate, ItemListInterface +{ + +} + +interface ItemList3 extends ItemList // do not report +{ + +} + +/** + * @extends \Traversable + */ +interface ResultStatement extends \Traversable +{ + +} + +interface Statement extends ResultStatement +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php new file mode 100644 index 0000000000..76cbd59de8 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php @@ -0,0 +1,36 @@ + + */ +interface ItemListInterface extends \Traversable +{ +} + +/** + * @implements \IteratorAggregate + */ +final class ItemList implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} + +/** + * @implements \IteratorAggregate + */ +final class ItemList2 implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php new file mode 100644 index 0000000000..1cda1bcbcd --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -0,0 +1,109 @@ += 8.1 + +namespace EnumGenericAncestors; + +interface NonGeneric +{ + +} + +/** + * @template T of object + * @template U + */ +interface Generic +{ + +} + +/** + * @implements NonGeneric + */ +enum Foo +{ + +} + +enum Foo2 implements NonGeneric +{ + +} + +/** + * @implements NonGeneric + */ +enum Foo3 implements NonGeneric +{ + +} + +enum Foo4 implements Generic +{ + +} + +/** + * @implements Generic<\stdClass, int> + */ +enum Foo5 implements Generic +{ + +} + +/** + * @implements Generic<\stdClass> + */ +enum Foo6 implements Generic +{ + +} + +/** + * @extends Generic<\stdClass, int> + */ +enum Foo7 +{ + +} + +/** + * @extends \Traversable + */ +interface TraversableInt extends \Traversable +{ + +} + +/** + * @implements \IteratorAggregate + */ +enum Foo8 implements TraversableInt, \IteratorAggregate +{ + + public function getIterator() + { + return new \ArrayIterator([]); + } + +} + +/** + * @implements Generic + */ +enum TypeProjection implements Generic +{ + +} + +/** + * @template T = string + */ +interface GenericDefault +{ + +} + +enum Foo9 implements GenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-template.php b/tests/PHPStan/Rules/Generics/data/enum-template.php new file mode 100644 index 0000000000..e55f6fe743 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/enum-template.php @@ -0,0 +1,25 @@ += 8.1 + +namespace EnumTemplate; + +/** + * @template T + */ +enum Foo +{ + +} + +/** + * @template T + * @template U + */ +enum Bar +{ + +} + +enum Baz +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index cb2e952b5d..8a1ff456f9 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -19,7 +19,7 @@ function bar() } /** - * @template T of int + * @template T of float */ function baz() { @@ -33,3 +33,91 @@ function lorem() { } + +/** @template T of bool */ +function ipsum() +{ + +} + +/** @template T of float */ +function dolor() +{ + +} + +/** @template T of resource */ +function resourceBound() +{ + +} + +/** @template T of array */ +function izumi() +{ + +} + +/** @template T of array{0: string, 1: bool} */ +function nakano() +{ + +} + +/** @template T of null */ +function nullNotSupported() +{ + +} + +/** @template T of ?int */ +function nullableUnionSupported() +{ + +} + +/** @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 e5a795b234..7f0da436e7 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -19,7 +19,7 @@ interface Bar } /** - * @template T of int + * @template T of float */ interface Baz { @@ -27,9 +27,77 @@ interface Baz } /** + * @phpstan-type ExportedAlias string * @template TypeAlias */ interface Lorem { } + +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ +interface Ipsum +{ + +} + +/** @template T */ +interface NormalT +{ + +} + +/** @template T of NormalT<\stdClass>|\stdClass */ +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 d11d3bb935..e7ffcbfaa2 100644 --- a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php @@ -2,35 +2,22 @@ namespace MethodSignatureVariance; -/** @template-covariant T */ -interface Out { -} - -/** @template T */ -interface Invariant { -} - -/** - * @template-covariant T - */ class C { /** - * @param Out $a - * @param Invariant $b - * @param T $c - * @return T + * @template U + * @return void */ - function a($a, $b, $c) { - 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 f7baec953d..edf5d62201 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -45,7 +45,7 @@ class Baz { /** - * @template T of int + * @template T of float */ public function doFoo() { @@ -54,6 +54,9 @@ public function doFoo() } +/** + * @phpstan-type ExportedAlias string + */ class Lorem { @@ -66,3 +69,77 @@ public function doFoo() } } + +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias + */ +class Ipsum +{ + + /** + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ + 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/nested-generic-types.php b/tests/PHPStan/Rules/Generics/data/nested-generic-types.php new file mode 100644 index 0000000000..2b96f54bd8 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/nested-generic-types.php @@ -0,0 +1,55 @@ + + */ +class Foo +{ + +} + +/** + * @template T + * @template U of SomeObjectInterface + */ +class Bar +{ + +} + +/** + * @template T of int + * @template U of SomeObjectInterface + */ +class Baz +{ + +} + +/** + * @template T + * @template U of NotGeneric + * @template V of MultipleGenerics<\stdClass> + * @template W of MultipleGenerics<\stdClass, \Exception, \SplFileInfo> + */ +class Lorem +{ + +} 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/scalar-class-name.php b/tests/PHPStan/Rules/Generics/data/scalar-class-name.php new file mode 100644 index 0000000000..363bbd154e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/scalar-class-name.php @@ -0,0 +1,56 @@ + + */ +class The implements Scalar +{ + /** + * @var T + */ + private $subject; + + /** + * @var Closure(T): mixed + */ + private $context; + + /** + * @param T $subject + * @param Closure(T): mixed $context + */ + public function __construct( + $subject, + $context + ) + { + $this->subject = $subject; + $this->context = $context; + } + /** + * @return T + */ + public function value() + { + ($this->context)($this->subject); + return $this->subject; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index bb11c78183..39126a88f2 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -19,7 +19,7 @@ trait Bar } /** - * @template T of int + * @template T of float */ trait Baz { @@ -27,9 +27,67 @@ trait Baz } /** + * @phpstan-type ExportedAlias string * @template TypeAlias */ trait Lorem { } + +/** + * @phpstan-type LocalAlias string + * @phpstan-import-type ExportedAlias from Lorem as ImportedAlias + * @template LocalAlias + * @template ExportedAlias + * @template ImportedAlias + */ +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 new file mode 100644 index 0000000000..f34fb5ffb9 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -0,0 +1,85 @@ + */ + use NongenericTrait; + + /** @use GenericTrait<\stdClass> */ + use GenericTrait; + +} + +class Bar +{ + + /** @use GenericTrait */ + use GenericTrait; + +} + +class Baz +{ + + use GenericTrait; + +} + +class Lorem +{ + + /** @use GenericTrait<\stdClass, \Exception> */ + use GenericTrait; + +} + +trait NestedTrait +{ + + /** @use NongenericTrait */ + use GenericTrait; + +} + +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 @@ + + */ +class ContinueBreakInLoopRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ContinueBreakInLoopRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/continue-break.php'], [ + [ + 'Keyword break used outside of a loop or a switch statement.', + 67, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 69, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 77, + ], + [ + 'Keyword continue used outside of a loop or a switch statement.', + 79, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 87, + ], + [ + 'Keyword break used outside of a loop or a switch statement.', + 95, + ], + ]); + } + + 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/continue-break.php b/tests/PHPStan/Rules/Keywords/data/continue-break.php new file mode 100644 index 0000000000..c702597a23 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/continue-break.php @@ -0,0 +1,96 @@ + @@ -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, ], ]); @@ -47,7 +45,6 @@ public function testBug3406(): void public function testBug3406ReflectionCheck(): void { - $this->createBroker(); $reflectionProvider = $this->createReflectionProvider(); $reflection = $reflectionProvider->getClass(ClassFoo::class); $this->assertSame(AbstractFoo::class, $reflection->getNativeMethod('myFoo')->getDeclaringClass()->getName()); @@ -59,4 +56,67 @@ public function testbug3406AnotherCase(): void $this->analyse([__DIR__ . '/data/bug-3406_2.php'], []); } + 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/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 04d32c4327..decb237d34 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3,41 +3,69 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\NullsafeCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class CallMethodsRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - /** @var bool */ - private $checkNullables; + private bool $checkNullables; - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - /** @var bool */ - private $checkExplicitMixed = false; + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($broker, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed); + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallMethodsRule( - $broker, - new FunctionCallParametersCheck($ruleLevelHelper, true, true, true, true), - $ruleLevelHelper, - true, - true + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, 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; @@ -71,6 +99,7 @@ public function testCallMethods(): void [ 'Call to method doFoo() on an unknown class Test\UnknownClass.', 63, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Result of method Test\Bar::returnsVoid() (void) is used.', @@ -195,10 +224,12 @@ public function testCallMethods(): void [ 'Call to method test() on an unknown class Test\FirstUnknownClass.', 312, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Call to method test() on an unknown class Test\SecondUnknownClass.', 312, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Cannot call method ipsum() on Test\Foo|null.', @@ -377,23 +408,31 @@ public function testCallMethods(): void 911, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Cannot call method foo() on class-string|object.', + 914, + ], + [ + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'foo\'} given.', + 915, + ], + [ + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'bar\'} given.', 916, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{object, \'bar\'} given.', 921, ], [ - 'Parameter #1 $ns of method SimpleXMLElement::children() expects string, int given.', + 'Parameter #1 $namespaceOrPrefix of method SimpleXMLElement::children() expects string|null, int given.', 942, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<0, max> given.', 964, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<1, max> given.', 987, ], [ @@ -415,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.', @@ -433,16 +475,16 @@ public function testCallMethods(): void 1379, ], [ - 'Only iterables can be unpacked, array|null given in argument #3.', - 1456, + 'Only iterables can be unpacked, array|null given in argument #5.', + 1459, ], [ - 'Only iterables can be unpacked, int given in argument #4.', - 1456, + 'Only iterables can be unpacked, int given in argument #6.', + 1460, ], [ - 'Only iterables can be unpacked, string given in argument #5.', - 1456, + 'Only iterables can be unpacked, string given in argument #7.', + 1461, ], [ 'Parameter #1 $s of method Test\ClassStringWithUpperBounds::doFoo() expects class-string, string given.', @@ -455,14 +497,73 @@ public function testCallMethods(): void [ 'Unable to resolve the template type T in call to method Test\ClassStringWithUpperBounds::doFoo()', 1490, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + '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|null given.', + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ - 'Parameter #1 $members of method Test\ParameterTypeCheckVerbosity::doBar() expects array string, \'code\' => string)>, array string)> given.', + 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, + "Array does not have offset 'id'.", + ], + [ + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', + 1657, + ], + [ + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, \'abc\' given.', + 1658, + ], + [ + 'Parameter #1 $date of method Test\HelloWorld3::sayHello() expects array|int, DateTimeInterface given.', + 1732, + ], + [ + 'Parameter #1 $a of method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar() expects array, array given.', + 1751, + ], + [ + 'Unable to resolve the template type T in call to method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar()', + 1751, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + 'Parameter #1 $code of method Test\\KeyOfParam::foo() expects \'jfk\'|\'lga\', \'sfo\' given.', + 1777, + ], + [ + 'Parameter #1 $code of method Test\\ValueOfParam::foo() expects \'John F. Kennedy…\'|\'La Guardia Airport\', \'Newark Liberty…\' given.', + 1802, + ], + [ + '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, ], ]); } @@ -662,19 +763,23 @@ public function testCallMethodsOnThisOnly(): void 867, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'foo\'} given.', + 915, + ], + [ + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'bar\'} given.', 916, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{object, \'bar\'} given.', 921, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<0, max> given.', 964, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<1, max> given.', 987, ], [ @@ -688,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.', @@ -716,14 +824,73 @@ public function testCallMethodsOnThisOnly(): void [ 'Unable to resolve the template type T in call to method Test\ClassStringWithUpperBounds::doFoo()', 1490, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + '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|null given.', + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ - 'Parameter #1 $members of method Test\ParameterTypeCheckVerbosity::doBar() expects array string, \'code\' => string)>, array string)> given.', + 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, + "Array does not have offset 'id'.", + ], + [ + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', + 1657, + ], + [ + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, \'abc\' given.', + 1658, + ], + [ + 'Parameter #1 $date of method Test\HelloWorld3::sayHello() expects array|int, DateTimeInterface given.', + 1732, + ], + [ + 'Parameter #1 $a of method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar() expects array, array given.', + 1751, + ], + [ + 'Unable to resolve the template type T in call to method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar()', + 1751, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + 'Parameter #1 $code of method Test\\KeyOfParam::foo() expects \'jfk\'|\'lga\', \'sfo\' given.', + 1777, + ], + [ + 'Parameter #1 $code of method Test\\ValueOfParam::foo() expects \'John F. Kennedy…\'|\'La Guardia Airport\', \'Newark Liberty…\' given.', + 1802, + ], + [ + '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, ], ]); } @@ -784,6 +951,10 @@ public function testClosureBind(): void 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', 19, ], + [ + 'Call to an undefined method CallClosureBind\Bar::barMethod().', + 23, + ], [ 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', 28, @@ -794,16 +965,21 @@ public function testClosureBind(): void ], [ 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', - 39, + 38, + ], + [ + '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; @@ -812,6 +988,10 @@ public function testArrowFunctionClosureBind(): void 'Call to an undefined method CallArrowFunctionBind\Foo::nonexistentMethod().', 27, ], + [ + 'Call to an undefined method CallArrowFunctionBind\Bar::barMethod().', + 29, + ], [ 'Call to an undefined method CallArrowFunctionBind\Foo::nonexistentMethod().', 31, @@ -861,10 +1041,30 @@ public function testCallVariadicMethods(): void 'Parameter #4 ...$strings of method CallVariadicMethods\Foo::doVariadicString() expects string, int given.', 42, ], + [ + 'Parameter #5 ...$strings of method CallVariadicMethods\Foo::doVariadicString() expects string, int given.', + 42, + ], + [ + 'Parameter #6 ...$strings of method CallVariadicMethods\Foo::doVariadicString() expects string, int given.', + 42, + ], + [ + 'Method CallVariadicMethods\Foo::doIntegerParameters() invoked with 3 parameters, 2 required.', + 43, + ], [ 'Parameter #1 $foo of method CallVariadicMethods\Foo::doIntegerParameters() expects int, string given.', 43, ], + [ + 'Parameter #2 $bar of method CallVariadicMethods\Foo::doIntegerParameters() expects int, string given.', + 43, + ], + [ + 'Method CallVariadicMethods\Foo::doIntegerParameters() invoked with 3 parameters, 2 required.', + 44, + ], [ 'Parameter #1 ...$strings of method CallVariadicMethods\Bar::variadicStrings() expects string, int given.', 85, @@ -1123,7 +1323,6 @@ public function dataIterable(): array /** * @dataProvider dataIterable - * @param bool $checkNullables */ public function testIterables(bool $checkNullables): void { @@ -1353,26 +1552,101 @@ public function testShadowedTraitMethod(): void $this->analyse([__DIR__ . '/data/shadowed-trait-method.php'], []); } - public function testExplicitMixed(): void + public function dataExplicitMixed(): array + { + return [ + [ + true, + [ + [ + '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, + ], + [ + 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, T given.', + 65, + ], + [ + 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doFoo() expects callable(mixed): void, Closure(int): void given.', + 133, + '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.', + 152, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @dataProvider dataExplicitMixed + * @param list $errors + */ + public function testExplicitMixed(bool $checkExplicitMixed, array $errors): 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/check-explicit-mixed.php'], [ - [ - 'Cannot call method foo() on mixed.', - 17, - ], + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/check-explicit-mixed.php'], $errors); + } + + public function dataImplicitMixed(): array + { + return [ [ - 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', - 43, + 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, + ], + ], ], [ - 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, T given.', - 65, + 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 @@ -1414,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; @@ -1430,4 +1699,1931 @@ public function testBug3445(): void ]); } + public function testBug3481(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3481.php'], [ + [ + 'Method Bug3481\Foo::doSomething() invoked with 2 parameters, 3 required.', + 34, + ], + [ + 'Parameter #1 $a of method Bug3481\Foo::doSomething() expects string, int|string given.', + 44, + ], + ]); + } + + public function testBug3683(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3683.php'], [ + [ + 'Parameter #1 $exception of method Generator::throw() expects Throwable, int given.', + 7, + ], + ]); + } + + public function testStringable(): 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/stringable.php'], []); + } + + public function testStringableStrictTypes(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/stringable-strict.php'], [ + [ + 'Parameter #1 $s of method TestStringables\Dolor::doFoo() expects string, TestStringables\Bar given.', + 15, + ], + ]); + } + + public function testMatchExpressionVoidIsUsed(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/match-expr-void-used.php'], [ + [ + 'Result of method MatchExprVoidUsed\Foo::doLorem() (void) is used.', + 10, + ], + [ + 'Result of method MatchExprVoidUsed\Foo::doBar() (void) is used.', + 11, + ], + ]); + } + + public function testNullSafe(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/nullsafe-method-call.php'], [ + [ + 'Method NullsafeMethodCall\Foo::doBar() invoked with 1 parameter, 0 required.', + 11, + ], + [ + 'Parameter #1 $passedByRef of method NullsafeMethodCall\Foo::doBaz() is passed by reference, so it expects variables only.', + 26, + ], + [ + '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, + ], + ]); + } + + public function testDisallowNamedArguments(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP earlier than 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/disallow-named-arguments.php'], [ + [ + 'Named arguments are supported only on PHP 8.0 and later.', + 10, + ], + ]); + } + + 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 (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/named-arguments.php'], [ + [ + 'Named argument cannot be followed by a positional argument.', + 21, + ], + [ + 'Named argument cannot be followed by a positional argument.', + 22, + ], + [ + 'Missing parameter $j (int) in call to method NamedArgumentsMethod\Foo::doFoo().', + 19, + ], + [ + 'Missing parameter $k (int) in call to method NamedArgumentsMethod\Foo::doFoo().', + 19, + ], + [ + 'Argument for parameter $i has already been passed.', + 26, + ], + [ + 'Argument for parameter $i has already been passed.', + 32, + ], + [ + 'Missing parameter $k (int) in call to method NamedArgumentsMethod\Foo::doFoo().', + 37, + ], + [ + 'Unknown parameter $z in call to method NamedArgumentsMethod\Foo::doFoo().', + 46, + ], + [ + 'Parameter #1 $i of method NamedArgumentsMethod\Foo::doFoo() expects int, string given.', + 50, + ], + [ + 'Parameter $j of method NamedArgumentsMethod\Foo::doFoo() expects int, string given.', + 57, + ], + [ + 'Parameter $i of method NamedArgumentsMethod\Foo::doBaz() is passed by reference, so it expects variables only.', + 70, + ], + [ + 'Parameter $i of method NamedArgumentsMethod\Foo::doBaz() is passed by reference, so it expects variables only.', + 71, + ], + [ + 'Named argument cannot be followed by an unpacked (...) argument.', + 73, + ], + [ + 'Parameter $j of method NamedArgumentsMethod\Foo::doFoo() expects int, string given.', + 75, + ], + [ + 'Named argument cannot be followed by a positional argument.', + 77, + ], + [ + 'Missing parameter $j (int) in call to method NamedArgumentsMethod\Foo::doFoo().', + 77, + ], + [ + 'Parameter #3 ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 87, + ], + [ + 'Parameter $b of method NamedArgumentsMethod\Foo::doIpsum() expects int, string given.', + 90, + ], + [ + 'Parameter $b of method NamedArgumentsMethod\Foo::doIpsum() expects int, string given.', + 91, + ], + [ + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 91, + ], + [ + 'Missing parameter $b (int) in call to method NamedArgumentsMethod\Foo::doIpsum().', + 92, + ], + [ + 'Missing parameter $a (int) in call to method NamedArgumentsMethod\Foo::doIpsum().', + 93, + ], + [ + '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 + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/bug-4199.php'], [ + [ + 'Cannot call method answer() on Bug4199\Baz|null.', + 37, + ], + ]); + } + + public function testBug4188(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/bug-4188.php'], []); + } + + public function testOnlyRelevantUnableToResolveTemplateType(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $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.', + 41, + ], + [ + 'Unable to resolve the template type T in call to method OnlyRelevantUnableToResolve\Foo::doBaz()', + 41, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + ]); + } + + public function testBug4552(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4552.php'], []); + } + + public function testBug2837(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-2837.php'], []); + } + + public function testBug2298(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-2298.php'], []); + } + + public function testBug1661(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-1661.php'], []); + } + + public function testBug1656(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-1656.php'], []); + } + + public function testBug3534(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3534.php'], []); + } + + public function testBug4557(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4557.php'], []); + } + + public function testBug4209(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209.php'], []); + } + + public function testBug4209Two(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209-2.php'], []); + } + + public function testBug3321(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3321.php'], []); + } + + public function testBug4498(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4498.php'], []); + } + + public function testBug3922(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3922.php'], [ + [ + 'Parameter #1 $query of method Bug3922\FooQueryHandler::handle() expects Bug3922\FooQuery, Bug3922\BarQuery given.', + 63, + ], + ]); + } + + public function testBug4642(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4642.php'], []); + } + + public function testBug4008(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4008.php'], []); + } + + public function testBug3546(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3546.php'], []); + } + + public function testBug4800(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + 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().', + 36, + ], + ]); + } + + public function testGenericReturnTypeResolvedToNever(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/generic-return-type-never.php'], [ + [ + 'Return type of call to method GenericReturnTypeNever\Foo::doBar() contains unresolvable type.', + 70, + ], + [ + 'Return type of call to method GenericReturnTypeNever\Foo::doBazBaz() contains unresolvable type.', + 73, + ], + ]); + } + + public function testUnableToResolveCallbackParameterType(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/unable-to-resolve-callback-parameter-type.php'], []); + } + + public function testBug4083(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4083.php'], []); + } + + public function testBug5253(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5253.php'], []); + } + + public function testBug4844(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4844.php'], []); + } + + public function testBug5258(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5258.php'], []); + } + + public function testBug5591(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5591.php'], []); + } + + public function testGenericObjectLowerBound(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/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', + ], + ]); + } + + public function testNonEmptyStringVerbosity(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/non-empty-string-verbosity.php'], [ + [ + 'Parameter #1 $i of method NonEmptyStringVerbosity\Foo::doBar() expects int, string given.', + 13, + ], + ]); + } + + public function testBug5536(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5536.php'], []); + } + + public function testBug5372(): void + { + $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', + ], + ]); + } + + public function testLiteralString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/literal-string.php'], [ + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, string given.', + 18, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, int given.', + 21, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, 1 given.', + 22, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, mixed given.', + 25, + ], + [ + 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + 58, + ], + [ + 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + 60, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', + 65, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, mixed given.', + 66, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, mixed given.', + 67, + ], + ]); + } + + public function testBug3555(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3555.php'], [ + [ + 'Parameter #1 $arg of method Bug3555\Enum::run() expects 1|2|3|4|5|6|7|8|9, 100 given.', + 28, + ], + ]); + } + + public function testBug3530(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3530.php'], []); + } + + public function testBug5562(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5562.php'], []); + } + + public function testBug4211(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4211.php'], []); + } + + public function testBug3514(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3514.php'], []); + } + + public function testBug3465(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3465.php'], []); + } + + public function testBug5868(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5868.php'], [ + [ + 'Cannot call method nullable1() on Bug5868\HelloWorld|null.', + 14, + ], + [ + 'Cannot call method nullable2() on Bug5868\HelloWorld|null.', + 15, + ], + [ + 'Cannot call method nullable3() on Bug5868\HelloWorld|null.', + 16, + ], + ]); + } + + public function testBug5460(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5460.php'], []); + } + + public function testFirstClassCallable(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-method-callable.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/call-method-in-enum.php'], [ + [ + 'Call to an undefined method CallMethodInEnum\Foo::doNonexistent().', + 11, + ], + [ + 'Call to an undefined method CallMethodInEnum\Bar::doNonexistent().', + 22, + ], + [ + '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, + ], + ]); + } + + public function testBug6239(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('This test needs PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6293.php'], []); + } + + public function testBug6306(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-6306.php'], []); + } + + public function testRectorDoWhileVarIssue(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/rector-do-while-var-issue.php'], [ + [ + 'Parameter #1 $cls of method RectorDoWhileVarIssue\Foo::processCharacterClass() expects string, int|string given.', + 24, + ], + ]); + } + + public function testReadOnlyPropertyPassedByReference(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/readonly-property-passed-by-reference.php'], [ + [ + 'Parameter #1 $param is passed by reference so it does not accept readonly property ReadonlyPropertyPassedByRef\Foo::$bar.', + 15, + ], + [ + 'Parameter $param is passed by reference so it does not accept readonly property ReadonlyPropertyPassedByRef\Foo::$bar.', + 16, + ], + ]); + } + + public function testBug6055(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6055.php'], []); + } + + public function testBug6081(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6081.php'], []); + } + + public function testBug6236(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6236.php'], []); + } + + public function testBug6118(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6118.php'], []); + } + + public function testBug6464(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6464.php'], []); + } + + public function testBug6423(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6423.php'], []); + } + + public function testBug5869(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5869.php'], []); + } + + public function testGenericsEmptyArray(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-empty-array.php'], []); + } + + public function testGenericsInferCollection(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ + [ + 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', + 43, + ], + [ + 'Parameter #1 $c of method GenericsInferCollection\Bar::doBar() expects GenericsInferCollection\ArrayCollection2, GenericsInferCollection\ArrayCollection2<(int|string), mixed> given.', + 62, + ], + [ + 'Parameter #1 $c of method GenericsInferCollection\Bar::doBar() expects GenericsInferCollection\ArrayCollection2, GenericsInferCollection\ArrayCollection2<(int|string), mixed> given.', + 63, + ], + [ + 'Parameter #1 $c of method GenericsInferCollection\Bar::doBar() expects GenericsInferCollection\ArrayCollection2, GenericsInferCollection\ArrayCollection2<(int|string), mixed> given.', + 64, + ], + ]); + } + + public function testGenericsInferCollectionLevel8(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ + [ + 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', + 43, + ], + ]); + } + + 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/CallPrivateMethodThroughStaticRuleTest.php b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php new file mode 100644 index 0000000000..25cd02b49b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php @@ -0,0 +1,29 @@ + + */ +class CallPrivateMethodThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallPrivateMethodThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-private-method-static.php'], [ + [ + 'Unsafe call to private method CallPrivateMethodThroughStatic\Foo::doBar() through static::.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index a900b4aefd..b9999f8610 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -3,37 +3,64 @@ namespace PHPStan\Rules\Methods; 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\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallStaticMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class CallStaticMethodsRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($broker, true, $this->checkThisOnly, true, false); + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallStaticMethodsRule( - $broker, - new FunctionCallParametersCheck($ruleLevelHelper, true, true, true, true), - $ruleLevelHelper, - new ClassCaseSensitivityCheck($broker), - 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'], [ [ @@ -71,6 +98,7 @@ public function testCallStaticMethods(): void [ 'Call to static method loremIpsum() on an unknown class CallStaticMethods\UnknownStaticMethodClass.', 67, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Call to private method __construct() of class CallStaticMethods\ClassWithConstructor.', @@ -216,13 +244,14 @@ public function testCallStaticMethods(): void 'Call to an undefined static method CallStaticMethods\Foo::nonexistentMethod().', 303, ], - [ - 'Call to static method doFoo() on trait CallStaticMethods\TraitWithStaticMethod.', - 323, - ], [ 'Call to static method doFoo() on an unknown class CallStaticMethods\TraitWithStaticMethod.', 328, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Call to an undefined static method CallStaticMethods\CallWithStatic::nonexistent().', + 344, ], ]); } @@ -360,4 +389,528 @@ public function testBug3448(): void ]); } + public function testBug3641(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-3641.php'], [ + [ + 'Static method Bug3641\Foo::bar() invoked with 1 parameter, 0 required.', + 32, + ], + ]); + } + + public function testBug2164(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-2164.php'], [ + [ + 'Parameter #1 $arg of static method Bug2164\A::staticTest() expects static(Bug2164\B)|string, Bug2164\B|string given.', + 24, + ], + ]); + } + + public function testNamedArguments(): void + { + $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().', + 15, + ], + [ + 'Unknown parameter $z in call to static method StaticMethodNamedArguments\Foo::doFoo().', + 16, + ], + ]); + } + + public function testBug577(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-577.php'], []); + } + + public function testBug4550(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-4550.php'], [ + [ + 'Parameter #1 $class of static method Bug4550\Test::valuesOf() expects class-string, string given.', + 34, + ], + [ + 'Parameter #1 $class of static method Bug4550\Test::valuesOf() expects class-string, string given.', + 44, + ], + ]); + } + + public function testBug1971(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.x'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-1971.php'], [ + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', + 16, + ], + ]); + } + + public function testBug1971Php8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-1971.php'], [ + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug1971\\\HelloWorld\', \'sayHello\'} given.', + 14, + ], + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello\'} given.', + 15, + ], + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', + 16, + ], + ]); + } + + public function testBug5259(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-5259.php'], []); + } + + public function testBug5536(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-5536.php'], []); + } + + public function testBug4886(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-4886.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->checkThisOnly = false; + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-static-method-callable.php'], []); + } + + public function testBug5893(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5893.php'], []); + } + + public function testBug6249(): void + { + // discussion https://github.com/phpstan/phpstan/discussions/6249 + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6249.php'], []); + } + + public function testBug5749(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5749.php'], []); + } + + public function testBug5757(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5757.php'], []); + } + + 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 new file mode 100644 index 0000000000..c7c0e8f89a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php @@ -0,0 +1,54 @@ + + */ +class CallToConstructorStatementWithoutSideEffectsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToConstructorStatementWithoutSideEffectsRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/constructor-statement-no-side-effects.php'], [ + [ + '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, + ], + [ + '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, + ], + ]); + } + + public function testBug4455(): void + { + $this->analyse([__DIR__ . '/data/bug-4455-constructor.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStamentWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStamentWithoutSideEffectsRuleTest.php deleted file mode 100644 index 2796cc5a77..0000000000 --- a/tests/PHPStan/Rules/Methods/CallToMethodStamentWithoutSideEffectsRuleTest.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ -class CallToMethodStamentWithoutSideEffectsRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new CallToMethodStamentWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects.php'], [ - [ - 'Call to method DateTimeImmutable::modify() on a separate line has no effect.', - 15, - ], - [ - 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', - 16, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php new file mode 100644 index 0000000000..67bd722602 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -0,0 +1,132 @@ + + */ +class CallToMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects.php'], [ + [ + 'Call to method DateTimeImmutable::modify() on a separate line has no effect.', + 15, + ], + [ + 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', + 16, + ], + [ + 'Call to method Exception::getCode() on a separate line has no effect.', + 21, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bar::doPure() on a separate line has no effect.', + 63, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bar::doPureWithThrowsVoid() on a separate line has no effect.', + 64, + ], + ]); + } + + public function testNullsafe(): void + { + $this->analyse([__DIR__ . '/data/nullsafe-method-call-statement-no-side-effects.php'], [ + [ + 'Call to method Exception::getMessage() on a separate line has no effect.', + 10, + ], + ]); + } + + public function testBug4232(): void + { + $this->analyse([__DIR__ . '/data/bug-4232.php'], []); + } + + 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.', + 55, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure2() on a separate line has no effect.', + 56, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure3() on a separate line has no effect.', + 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, + ], + ]); + } + + public function testBug4455(): void + { + $this->analyse([__DIR__ . '/data/bug-4455.php'], []); + } + + public function testBug11503(): void + { + $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.', + 12, + ], + [ + 'Call to method FirstClassCallableMethodWithoutSideEffect\Bar::doFoo() on a separate line has no effect.', + 36, + ], + [ + 'Call to method FirstClassCallableMethodWithoutSideEffect\Bar::doBar() on a separate line has no effect.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRuleTest.php deleted file mode 100644 index af43a6dcac..0000000000 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRuleTest.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class CallToStaticMethodStamentWithoutSideEffectsRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - $broker = $this->createReflectionProvider(); - return new CallToStaticMethodStamentWithoutSideEffectsRule( - new RuleLevelHelper($broker, true, false, true, false), - $broker - ); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/static-method-call-statement-no-side-effects.php'], [ - [ - '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, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php new file mode 100644 index 0000000000..1fa0216870 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -0,0 +1,91 @@ + + */ +class CallToStaticMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new CallToStaticMethodStatementWithoutSideEffectsRule( + new RuleLevelHelper($broker, true, false, true, false, false, false, true), + $broker, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-method-call-statement-no-side-effects.php'], [ + [ + 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', + 12, + ], + [ + 'Call to method DateTime::format() on a separate line has no effect.', + 23, + ], + ]); + } + + 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.', + 55, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure2() on a separate line has no effect.', + 56, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure3() on a separate line has no effect.', + 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.', + 85, + ], + ]); + } + + public function testBug4455(): void + { + $this->analyse([__DIR__ . '/data/bug-4455-static.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->analyse([__DIR__ . '/data/first-class-callable-static-method-without-side-effect.php'], [ + [ + 'Call to static method FirstClassCallableStaticMethodWithoutSideEffect\Foo::doFoo() on a separate line has no effect.', + 12, + ], + [ + 'Call to static method FirstClassCallableStaticMethodWithoutSideEffect\Bar::doFoo() on a separate line has no effect.', + 36, + ], + [ + 'Call to static method FirstClassCallableStaticMethodWithoutSideEffect\Bar::doBar() on a separate line has no effect.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/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 7db67a1ef6..170920bbf6 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -2,73 +2,93 @@ namespace PHPStan\Rules\Methods; +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; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInTypehintsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private int $phpVersionId = PHP_VERSION_ID; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), 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'], [ [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::foo() has invalid type TestMethodTypehints\NonexistentClass.', + 'Method TestMethodTypehints\FooMethodTypehints::foo() has invalid return type TestMethodTypehints\NonexistentClass.', 8, ], [ - 'Parameter $bar of method TestMethodTypehints\FooMethodTypehints::bar() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bar of method TestMethodTypehints\FooMethodTypehints::bar() has invalid type TestMethodTypehints\BarMethodTypehints.', 13, ], [ - 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::lorem() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::lorem() has invalid type TestMethodTypehints\BarMethodTypehints.', 28, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::lorem() has invalid type TestMethodTypehints\BazMethodTypehints.', + 'Method TestMethodTypehints\FooMethodTypehints::lorem() has invalid return type TestMethodTypehints\BazMethodTypehints.', 28, ], [ - 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid type TestMethodTypehints\BarMethodTypehints.', 38, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid type TestMethodTypehints\BazMethodTypehints.', + 'Method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid return type TestMethodTypehints\BazMethodTypehints.', 38, ], [ - 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::dolor() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::dolor() has invalid type TestMethodTypehints\BarMethodTypehints.', 48, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::dolor() has invalid type TestMethodTypehints\BazMethodTypehints.', + 'Method TestMethodTypehints\FooMethodTypehints::dolor() has invalid return type TestMethodTypehints\BazMethodTypehints.', 48, ], [ - 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid typehint type parent.', + 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid type parent.', 53, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid type parent.', + 'Method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid return type parent.', 53, ], [ - 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid typehint type parent.', + 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid type parent.', 62, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid type parent.', + 'Method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid return type parent.', 62, ], [ @@ -83,10 +103,18 @@ public function testExistingClassInTypehint(): void 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehints.', 76, ], + [ + 'Class stdClass referenced with incorrect case: STDClass.', + 76, + ], [ 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehintS.', 76, ], + [ + 'Class stdClass referenced with incorrect case: stdclass.', + 76, + ], [ 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\FOOMethodTypehints.', 85, @@ -104,12 +132,20 @@ public function testExistingClassInTypehint(): void 94, ], [ - 'Parameter $array of method TestMethodTypehints\FooMethodTypehints::unknownTypesInArrays() has invalid typehint type TestMethodTypehints\NonexistentClass.', + 'Parameter $array of method TestMethodTypehints\FooMethodTypehints::unknownTypesInArrays() has invalid type TestMethodTypehints\AnotherNonexistentClass.', 102, ], [ - 'Parameter $array of method TestMethodTypehints\FooMethodTypehints::unknownTypesInArrays() has invalid typehint type TestMethodTypehints\AnotherNonexistentClass.', - 102, + 'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid type TestMethodTypehints\Bla.', + 113, + ], + [ + 'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid type TestMethodTypehints\Ble.', + 113, + ], + [ + 'Template type U of method TestMethodTypehints\TemplateTypeMissingInParameter::doFoo() is not referenced in a parameter.', + 130, ], ]); } @@ -118,11 +154,11 @@ public function testExistingClassInIterableTypehint(): void { $this->analyse([__DIR__ . '/data/typehints-iterable.php'], [ [ - 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid typehint type TestMethodTypehints\NonexistentClass.', + 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid type TestMethodTypehints\NonexistentClass.', 11, ], [ - 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid typehint type TestMethodTypehints\AnotherNonexistentClass.', + 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid type TestMethodTypehints\AnotherNonexistentClass.', 11, ], ]); @@ -130,15 +166,452 @@ 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 typehint type void.', + 'Parameter $param of method VoidParameterTypehintMethod\Foo::doFoo() has invalid type void.', + 8, + ], + ]); + } + + public function dataNativeUnionTypes(): array + { + return [ + [ + 70400, + [ + [ + 'Method NativeUnionTypesSupport\Foo::doFoo() uses native union types but they\'re supported only on PHP 8.0 and later.', + 8, + ], + [ + 'Method NativeUnionTypesSupport\Foo::doBar() uses native union types but they\'re supported only on PHP 8.0 and later.', + 13, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataNativeUnionTypes + * @param list $errors + */ + public function testNativeUnionTypes(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/native-union-types.php'], $errors); + } + + 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, + [ + [ + '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.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, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRequiredParameterAfterOptional + * @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); + } + + public function testBug4641(): void + { + $this->analyse([__DIR__ . '/data/bug-4641.php'], [ + [ + 'Template type U of method Bug4641\I::getRepository() is not referenced in a parameter.', + 26, + ], + ]); + } + + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of method MethodIntersectionTypes\FooClass::doBar() has unresolvable native type.', + 33, + ], + [ + 'Method MethodIntersectionTypes\FooClass::doBar() has unresolvable native return type.', + 33, + ], + [ + 'Parameter $a of method MethodIntersectionTypes\FooClass::doBaz() has unresolvable native type.', + 38, + ], + [ + 'Method MethodIntersectionTypes\FooClass::doBaz() has unresolvable native return type.', + 38, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param list $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/enums-typehints.php'], [ + [ + 'Parameter $int of method EnumsTypehints\Foo::doFoo() has invalid type EnumsTypehints\intt.', 8, ], ]); } + 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 1ac88bc4e2..0f6d5b81f1 100644 --- a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class IncompatibleDefaultParameterTypeRuleTest extends RuleTestCase { @@ -35,4 +36,51 @@ public function testTraitCrash(): void $this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-trait-crash.php'], []); } + public function testBug4011(): void + { + $this->analyse([__DIR__ . '/data/bug-4011.php'], []); + } + + public function testBug2573(): void + { + $this->analyse([__DIR__ . '/data/bug-2573.php'], []); + } + + public function testNewInInitializers(): void + { + $this->analyse([__DIR__ . '/data/new-in-initializers.php'], [ + [ + 'Default value of the parameter #1 $i (stdClass) of method MethodNewInInitializers\Foo::doFoo() is incompatible with type int.', + 11, + ], + ]); + } + + public function testDefaultValueForPromotedProperty(): void + { + $this->analyse([__DIR__ . '/data/default-value-for-promoted-property.php'], [ + [ + 'Default value of the parameter #1 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 9, + ], + [ + 'Default value of the parameter #2 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 10, + ], + [ + '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 new file mode 100644 index 0000000000..b2a85a50c3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -0,0 +1,65 @@ + + */ +class MethodAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new MethodAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/method-attributes.php'], [ + [ + 'Attribute class MethodAttributes\Foo does not have the method target.', + 26, + ], + ]); + } + + public function testBug5898(): void + { + $this->analyse([__DIR__ . '/data/bug-5898.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php new file mode 100644 index 0000000000..8b558e4512 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -0,0 +1,86 @@ + + */ +class MethodCallableRuleTest extends RuleTestCase +{ + + private int $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true); + + return new MethodCallableRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new PhpVersion($this->phpVersion), + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + $this->analyse([__DIR__ . '/data/method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/method-callable.php'], [ + [ + 'Call to method MethodCallable\Foo::doFoo() with incorrect case: dofoo', + 11, + ], + [ + 'Call to an undefined method MethodCallable\Foo::doNonexistent().', + 12, + ], + [ + 'Cannot call method doFoo() on int.', + 13, + ], + [ + 'Call to private method doBar() of class MethodCallable\Bar.', + 18, + ], + [ + 'Call to method doFoo() on an unknown class MethodCallable\Nonexistent.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Call to private method doFoo() of class MethodCallable\ParentClass.', + 53, + ], + [ + 'Creating callable from a non-native method MethodCallable\Lorem::doBar().', + 66, + ], + [ + 'Creating callable from a non-native method MethodCallable\Ipsum::doBar().', + 85, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index fbfa4bbb44..2d7d7f1a5e 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -3,26 +3,35 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MethodSignatureRuleTest extends \PHPStan\Testing\RuleTestCase +class MethodSignatureRuleTest extends RuleTestCase { - /** @var bool */ - private $reportMaybes; + private bool $reportMaybes; - /** @var bool */ - private $reportStatic; + private bool $reportStatic; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { + $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), - true + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic), + true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + $phpClassReflectionExtension, + false, ); } @@ -71,7 +80,7 @@ public function testReturnTypeRule(): void 'Parameter #1 $node (PhpParser\Node\Expr\StaticCall) of method MethodSignature\Rule::processNode() should be contravariant with parameter $node (PhpParser\Node) of method MethodSignature\GenericRule::processNode()', 454, ], - ] + ], ); } @@ -116,7 +125,7 @@ public function testReturnTypeRuleTrait(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClassUsingTrait::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 103, ], - ] + ], ); } @@ -145,7 +154,7 @@ public function testReturnTypeRuleTraitWithoutMaybes(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClassUsingTrait::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 103, ], - ] + ], ); } @@ -174,7 +183,7 @@ public function testReturnTypeRuleWithoutMaybes(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClass::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 358, ], - ] + ], ); } @@ -204,4 +213,372 @@ public function testBug2950(): void $this->analyse([__DIR__ . '/data/bug-2950.php'], []); } + public function testBug3997(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $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()', + 35, + ], + [ + 'Return type (int) of method Bug3997\Lorem::count() should be covariant with return type (int<0, max>) of method Countable::count()', + 49, + ], + [ + 'Return type (string) of method Bug3997\Ipsum::count() should be compatible with return type (int<0, max>) of method Countable::count()', + 63, + ], + ]); + } + + public function testBug4003(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4003.php'], [ + [ + 'Return type (string) of method Bug4003\Baz::foo() should be compatible with return type (int) of method Bug4003\Boo::foo()', + 15, + ], + [ + 'Parameter #1 $test (string) of method Bug4003\Ipsum::doFoo() should be compatible with parameter $test (int) of method Bug4003\Lorem::doFoo()', + 38, + ], + ]); + } + + public function testBug4017(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4017.php'], []); + } + + public function testBug4017Two(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4017_2.php'], [ + [ + 'Parameter #1 $a (Bug4017_2\Foo) of method Bug4017_2\Lorem::doFoo() should be compatible with parameter $a (stdClass) of method Bug4017_2\Bar::doFoo()', + 51, + ], + ]); + } + + public function testBug4017Three(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4017_3.php'], [ + [ + 'Parameter #1 $a (T of stdClass) of method Bug4017_3\Lorem::doFoo() should be compatible with parameter $a (Bug4017_3\Foo) of method Bug4017_3\Bar::doFoo()', + 45, + ], + ]); + } + + public function testBug4023(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4023.php'], []); + } + + public function testBug4006(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4006.php'], []); + } + + public function testBug4084(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4084.php'], []); + } + + public function testBug3523(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-3523.php'], [ + [ + 'Return type (Bug3523\Baz) of method Bug3523\Baz::deserialize() should be covariant with return type (static(Bug3523\FooInterface)) of method Bug3523\FooInterface::deserialize()', + 40, + ], + ]); + } + + public function testBug4707(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4707.php'], [ + [ + 'Return type (list) of method Bug4707\Block2::getChildren() should be compatible with return type (list>) of method Bug4707\ParentNodeInterface::getChildren()', + 38, + ], + ]); + } + + public function testBug4707Covariant(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4707-covariant.php'], [ + [ + 'Return type (list) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (list>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', + 38, + ], + ]); + } + + public function testBug4707Two(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4707-two.php'], []); + } + + public function testBug4729(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4729.php'], []); + } + + public function testBug4854(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4854.php'], []); + } + + public function testMemcachePoolGet(): void + { + $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/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 bf15a9f3b7..babaadaae2 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -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/missing-method-impl.php'], [ [ 'Non-abstract class MissingMethodImpl\Baz contains abstract method doBaz() from class MissingMethodImpl\Baz.', @@ -32,10 +29,39 @@ 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, ], ]); } + public function testBug3469(): void + { + $this->analyse([__DIR__ . '/data/bug-3469.php'], []); + } + + public function testBug3958(): void + { + $this->analyse([__DIR__ . '/data/bug-3958.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-method-impl-enum.php'], [ + [ + 'Enum MissingMethodImplEnum\Bar contains abstract method doFoo() from interface MissingMethodImplEnum\FooInterface.', + 21, + ], + ]); + } + + 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 cc4647f4af..fce941c3ce 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -3,66 +3,147 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingMethodParameterTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingMethodParameterTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodParameterTypehintRule(new MissingTypehintCheck($broker, true, true)); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void { - $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], [ + $errors = [ [ - 'Method MissingMethodParameterTypehint\FooInterface::getFoo() has parameter $p1 with no typehint specified.', + 'Method MissingMethodParameterTypehint\FooInterface::getFoo() has parameter $p1 with no type specified.', 8, ], [ - 'Method MissingMethodParameterTypehint\FooParent::getBar() has parameter $p2 with no typehint specified.', + 'Method MissingMethodParameterTypehint\FooParent::getBar() has parameter $p2 with no type specified.', 15, ], [ - 'Method MissingMethodParameterTypehint\Foo::getFoo() has parameter $p1 with no typehint specified.', + 'Method MissingMethodParameterTypehint\Foo::getFoo() has parameter $p1 with no type specified.', 25, ], [ - 'Method MissingMethodParameterTypehint\Foo::getBar() has parameter $p2 with no typehint specified.', + 'Method MissingMethodParameterTypehint\Foo::getBar() has parameter $p2 with no type specified.', 33, ], [ - 'Method MissingMethodParameterTypehint\Foo::getBaz() has parameter $p3 with no typehint specified.', + 'Method MissingMethodParameterTypehint\Foo::getBaz() has parameter $p3 with no type specified.', 42, ], [ 'Method MissingMethodParameterTypehint\Foo::unionTypeWithUnknownArrayValueTypehint() has parameter $a with no value type specified in iterable type array.', 58, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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); + } + + public function testPromotedProperties(): void + { + $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::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method MissingTypehintPromotedProperties\Bar::__construct() has parameter $foo with no value type specified in iterable type array.', + 21, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + ]); + } + + public function testDeepInspectTypes(): void + { + $this->analyse([__DIR__ . '/data/deep-inspect-types.php'], [ + [ + 'Method DeepInspectTypes\Foo::doFoo() has parameter $foo with no value type specified in iterable type iterable.', + 11, + 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, + ], + ]); + } + + public function testBug3723(): void + { + $this->analyse([__DIR__ . '/data/bug-3723.php'], []); + } + + public function testBug6472(): void + { + $this->analyse([__DIR__ . '/data/bug-6472.php'], []); + } + + public function testFilterIteratorChildClass(): void + { + $this->analyse([__DIR__ . '/data/filter-iterator-child-class.php'], []); + } + + 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 359d7f4696..572b38c33a 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -3,52 +3,60 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingMethodReturnTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingMethodReturnTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodReturnTypehintRule(new MissingTypehintCheck($broker, true, true)); + return new MissingMethodReturnTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void { $this->analyse([__DIR__ . '/data/missing-method-return-typehint.php'], [ [ - 'Method MissingMethodReturnTypehint\FooInterface::getFoo() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\FooInterface::getFoo() has no return type specified.', 8, ], [ - 'Method MissingMethodReturnTypehint\FooParent::getBar() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\FooParent::getBar() has no return type specified.', 15, ], [ - 'Method MissingMethodReturnTypehint\Foo::getFoo() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\Foo::getFoo() has no return type specified.', 25, ], [ - 'Method MissingMethodReturnTypehint\Foo::getBar() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\Foo::getBar() has no return type specified.', 33, ], [ 'Method MissingMethodReturnTypehint\Foo::unionTypeWithUnknownArrayValueTypehint() return type has no value type specified in iterable type array.', 46, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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, ], ]); } @@ -58,4 +66,59 @@ public function testIndirectInheritanceBug2740(): void $this->analyse([__DIR__ . '/data/bug2740.php'], []); } + public function testArrayTypehintWithoutNullInPhpDoc(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/array-typehint-without-null-in-phpdoc.php'], []); + } + + public function testBug4415(): void + { + $this->analyse([__DIR__ . '/data/bug-4415.php'], []); + } + + public function testBug5089(): void + { + $this->analyse([__DIR__ . '/data/bug-5089.php'], []); + } + + 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 new file mode 100644 index 0000000000..5c308ea5ad --- /dev/null +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php @@ -0,0 +1,73 @@ + + */ +class NullsafeMethodCallRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NullsafeMethodCallRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/nullsafe-method-call-rule.php'], [ + [ + 'Using nullsafe method call on non-nullable type Exception. Use -> instead.', + 16, + ], + ]); + } + + 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 cdde0eaa67..c63d2ea3eb 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -3,8 +3,11 @@ 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_values; use const PHP_VERSION_ID; /** @@ -13,15 +16,24 @@ class OverridingMethodRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId; + 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), - false + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, true, true), + false, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + $phpClassReflectionExtension, + $this->checkMissingOverrideMethodAttribute, ); } @@ -43,17 +55,10 @@ public function dataOverridingFinalMethod(): array /** * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion - * @param string $contravariantMessage */ public function testOverridingFinalMethod(int $phpVersion, string $contravariantMessage): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - - $this->phpVersionId = $phpVersion; - $this->analyse([__DIR__ . '/data/overriding-method.php'], [ + $errors = [ [ 'Method OverridingFinalMethod\Bar::doFoo() overrides final method OverridingFinalMethod\Foo::doFoo().', 43, @@ -87,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 +127,22 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 'Method OverridingFinalMethod\SomeOtherException::__construct() overrides final method OverridingFinalMethod\OtherException::__construct().', 280, ], - ]); + [ + '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, + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $errors = array_values(array_filter($errors, static fn (array $error): bool => $error[1] !== 125)); + } + + $this->phpVersionId = $phpVersion; + $this->analyse([__DIR__ . '/data/overriding-method.php'], $errors); } public function dataParameterContravariance(): array @@ -209,18 +229,16 @@ public function dataParameterContravariance(): array /** * @dataProvider dataParameterContravariance - * @param string $file - * @param int $phpVersion - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testParameterContravariance( string $file, int $phpVersion, - array $expectedErrors + 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; @@ -273,34 +291,22 @@ public function dataReturnTypeCovariance(): array /** * @dataProvider dataReturnTypeCovariance - * @param int $phpVersion - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testReturnTypeCovariance( int $phpVersion, - array $expectedErrors + array $expectedErrors, ): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - $this->phpVersionId = $phpVersion; $this->analyse([__DIR__ . '/data/return-type-covariance.php'], $expectedErrors); } /** * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion - * @param string $contravariantMessage - * @param string $covariantMessage */ 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'], [ [ @@ -322,7 +328,6 @@ public function testVariadicParameterIsAlwaysOptional(): void /** * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion */ public function testBug3403(int $phpVersion): void { @@ -336,4 +341,497 @@ public function testBug3443(): void $this->analyse([__DIR__ . '/data/bug-3443.php'], []); } + public function testBug3478(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-3478.php'], []); + } + + public function testBug3629(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-3629.php'], []); + } + + public function testVariadics(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $errors = [ + [ + 'Parameter #2 $lang of method OverridingVariadics\OtherTranslator::translate() is not optional.', + 34, + ], + [ + 'Parameter #2 $lang of method OverridingVariadics\AnotherTranslator::translate() is not optional.', + 44, + ], + [ + 'Parameter #3 $parameters of method OverridingVariadics\AnotherTranslator::translate() is not variadic.', + 44, + ], + [ + 'Parameter #2 $lang of method OverridingVariadics\YetAnotherTranslator::translate() is not variadic.', + 54, + ], + ]; + + $this->analyse([__DIR__ . '/data/overriding-variadics.php'], $errors); + } + + public function dataLessOverridenParametersWithVariadic(): array + { + return [ + [ + 70400, + [ + [ + 'Parameter #1 $everything of method LessParametersVariadics\Bar::doFoo() is variadic but parameter #1 $many of method LessParametersVariadics\Foo::doFoo() is not variadic.', + 18, + ], + [ + 'Method LessParametersVariadics\Bar::doFoo() overrides method LessParametersVariadics\Foo::doFoo() but misses parameter #2 $parameters.', + 18, + ], + [ + 'Method LessParametersVariadics\Bar::doFoo() overrides method LessParametersVariadics\Foo::doFoo() but misses parameter #3 $here.', + 18, + ], + [ + 'Parameter #1 $everything of method LessParametersVariadics\Baz::doFoo() is variadic but parameter #1 $many of method LessParametersVariadics\Foo::doFoo() is not variadic.', + 28, + ], + [ + 'Method LessParametersVariadics\Baz::doFoo() overrides method LessParametersVariadics\Foo::doFoo() but misses parameter #2 $parameters.', + 28, + ], + [ + 'Method LessParametersVariadics\Baz::doFoo() overrides method LessParametersVariadics\Foo::doFoo() but misses parameter #3 $here.', + 28, + ], + [ + 'Parameter #2 $everything of method LessParametersVariadics\Lorem::doFoo() is variadic but parameter #2 $parameters of method LessParametersVariadics\Foo::doFoo() is not variadic.', + 38, + ], + [ + 'Method LessParametersVariadics\Lorem::doFoo() overrides method LessParametersVariadics\Foo::doFoo() but misses parameter #3 $here.', + 38, + ], + ], + ], + [ + 80000, + [ + [ + 'Parameter #1 ...$everything (int) of method LessParametersVariadics\Baz::doFoo() is not contravariant with parameter #2 $parameters (string) of method LessParametersVariadics\Foo::doFoo().', + 28, + ], + [ + 'Parameter #1 ...$everything (int) of method LessParametersVariadics\Baz::doFoo() is not contravariant with parameter #3 $here (string) of method LessParametersVariadics\Foo::doFoo().', + 28, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataLessOverridenParametersWithVariadic + * @param list $errors + */ + public function testLessOverridenParametersWithVariadic(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/less-parameters-variadics.php'], $errors); + } + + public function dataParameterTypeWidening(): array + { + return [ + [ + 70100, + [ + [ + 'Parameter #1 $foo (mixed) of method ParameterTypeWidening\Bar::doFoo() does not match parameter #1 $foo (string) of method ParameterTypeWidening\Foo::doFoo().', + 18, + ], + ], + ], + [ + 70200, + [], + ], + ]; + } + + /** + * @dataProvider dataParameterTypeWidening + * @param list $errors + */ + public function testParameterTypeWidening(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/parameter-type-widening.php'], $errors); + } + + public function testBug4516(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-4516.php'], []); + } + + public function dataTentativeReturnTypes(): array + { + return [ + [70400, []], + [80000, []], + [ + 80100, + [ + [ + 'Return type mixed of method TentativeReturnTypes\Foo::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', + 8, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + 'Return type string of method TentativeReturnTypes\Lorem::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', + 40, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + [ + '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.', + ], + ], + ], + ]; + } + + /** + * @dataProvider dataTentativeReturnTypes + * @param list $errors + */ + public function testTentativeReturnTypes(int $phpVersionId, array $errors): void + { + if (PHP_VERSION_ID < 80100) { + $errors = []; + } + if ($phpVersionId > PHP_VERSION_ID) { + $this->markTestSkipped(); + } + + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/tentative-return-types.php'], $errors); + } + + public function testCountableBug(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/countable-bug.php'], []); + } + + public function testBug6264(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-6264.php'], []); + } + + 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 c38b671db4..ec7bf57f05 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -3,17 +3,26 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ReturnTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + private bool $checkUnionTypes = true; + + private bool $checkBenevolentUnionTypes = false; + + protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false, $this->checkBenevolentUnionTypes, true))); } public function testReturnTypeRule(): void @@ -177,109 +186,128 @@ public function testReturnTypeRule(): void ], [ 'Method ReturnTypes\ReturnTernary::returnTernary() should return ReturnTypes\Foo but returns false.', - 625, + 627, ], [ 'Method ReturnTypes\TrickyVoid::returnVoidOrInt() should return int|void but returns string.', - 656, - ], - [ - 'Method ReturnTypes\TernaryWithJsonEncode::toJsonOrNull() should return string|null but returns string|false|null.', - 671, - ], - [ - 'Method ReturnTypes\TernaryWithJsonEncode::toJson() should return string but returns string|false.', - 684, + 658, ], [ 'Method ReturnTypes\TernaryWithJsonEncode::toJson() should return string but returns string|false.', - 687, + 689, ], [ 'Method ReturnTypes\AppendedArrayReturnType::foo() should return array but returns array.', - 700, + 702, ], [ 'Method ReturnTypes\AppendedArrayReturnType::bar() should return array but returns array.', - 710, + 712, ], [ 'Method ReturnTypes\WrongMagicMethods::__toString() should return string but returns true.', - 720, + 722, ], [ 'Method ReturnTypes\WrongMagicMethods::__isset() should return bool but returns int.', - 725, + 727, ], [ 'Method ReturnTypes\WrongMagicMethods::__destruct() with return type void returns int but should not return anything.', - 730, + 732, ], [ 'Method ReturnTypes\WrongMagicMethods::__unset() with return type void returns int but should not return anything.', - 735, + 737, ], [ 'Method ReturnTypes\WrongMagicMethods::__sleep() should return array but returns array.', - 740, + 742, ], [ 'Method ReturnTypes\WrongMagicMethods::__wakeup() with return type void returns int but should not return anything.', - 747, + 749, ], [ 'Method ReturnTypes\WrongMagicMethods::__set_state() should return object but returns array.', - 752, + 754, ], [ 'Method ReturnTypes\WrongMagicMethods::__clone() with return type void returns int but should not return anything.', - 757, + 759, ], [ - 'Method ReturnTypes\ReturnSpecifiedMethodCall::doFoo() should return string but returns string|false.', - 776, - ], - [ - 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', - 815, + '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.', - 838, + 839, ], [ - 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', - 858, + 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', + 859, ], [ 'Method ReturnTypes\NestedArrayCheck::doBar() should return array but returns array>.', - 873, + 874, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns string.', - 948, + 949, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns ReturnTypes\integer.', - 951, + 952, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doFoo() should return int but returns int|string.', - 1009, + 1010, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doBar() should return int but returns int|string.', - 1024, + 1025, ], [ 'Method ReturnTypes\ReturnStaticGeneric::instanceReturnsStatic() should return static(ReturnTypes\ReturnStaticGeneric) but returns ReturnTypes\ReturnStaticGeneric.', - 1064, + 1065, + ], + [ + 'Method ReturnTypes\NeverReturn::doFoo() should never return but return statement found.', + 1240, + ], + [ + 'Method ReturnTypes\NeverReturn::doBaz3() should never return but return statement found.', + 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.', @@ -359,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.', @@ -370,4 +395,875 @@ public function testReturnTypeRulePhp70(): void ]); } + public function testBug3997(): void + { + $this->analyse([__DIR__ . '/data/bug-3997.php'], [ + [ + "Method Bug3997\Foo::count() should return int<0, max> but returns 'foo'.", + 13, + ], + [ + "Method Bug3997\Bar::count() should return int<0, max> but returns 'foo'.", + 24, + ], + [ + 'Method Bug3997\Baz::count() should return int but returns string.', + 38, + ], + [ + 'Method Bug3997\Lorem::count() should return int but returns string.', + 52, + ], + [ + 'Method Bug3997\Dolor::count() should return int<0, max> but returns -1.', + 78, + ], + ]); + } + + public function testBug1903(): void + { + $this->analyse([__DIR__ . '/data/bug-1903.php'], [ + [ + 'Method Bug1903\Test::doFoo() should return array but returns int.', + 19, + ], + ]); + } + + public function testBug3117(): void + { + $this->analyse([__DIR__ . '/data/bug-3117.php'], [ + [ + '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.', + ], + ]); + } + + 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'], []); + } + + 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.', + 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.', + 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.', + 63, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + ]); + } + + public function testTemplateStringBound(): void + { + $this->analyse([__DIR__ . '/data/template-string-bound.php'], []); + } + + public function testBug4605(): void + { + $this->analyse([__DIR__ . '/data/bug-4605.php'], []); + } + + public function testReturnStatic(): void + { + $this->analyse([__DIR__ . '/data/return-static.php'], []); + } + + public function testBug4648(): void + { + $this->analyse([__DIR__ . '/data/bug-4648.php'], []); + } + + public function testBug3523(): void + { + $this->analyse([__DIR__ . '/data/bug-3523.php'], [ + [ + 'Method Bug3523\Bar::deserialize() should return static(Bug3523\Bar) but returns Bug3523\Bar.', + 31, + ], + ]); + } + + public function testBug3120(): void + { + $this->analyse([__DIR__ . '/data/bug-3120.php'], []); + } + + public function testBug3118(): void + { + $this->analyse([__DIR__ . '/data/bug-3118.php'], [ + [ + 'Method Bug3118\CustomEnum2::all() should return Bug3118\EnumSet but returns Bug3118\CustomEnumSet.', + 56, + ], + ]); + } + + public function testBug4795(): void + { + $this->analyse([__DIR__ . '/data/bug-4795.php'], []); + } + + public function testBug4803(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4803.php'], []); + } + + public function testBug7020(): void + { + $this->analyse([__DIR__ . '/data/bug-7020.php'], []); + } + + public function testBug2573(): void + { + $this->analyse([__DIR__ . '/data/bug-2573-return.php'], []); + } + + public function testBug4603(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-4603.php'], []); + } + + public function testBug3151(): void + { + $this->analyse([__DIR__ . '/data/bug-3151.php'], []); + } + + public function testTemplateUnion(): void + { + $this->analyse([__DIR__ . '/data/return-template-union.php'], [ + [ + 'Method ReturnTemplateUnion\Foo::doFoo2() should return T of bool|float|int|string but returns (T of bool|float|int|string)|null.', + 25, + ], + ]); + } + + public function dataBug5218(): array + { + return [ + [ + true, + [ + [ + 'Method Bug5218\IA::getIterator() should return Traversable but returns ArrayIterator.', + 14, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @dataProvider dataBug5218 + * @param list $errors + */ + public function testBug5218(bool $checkExplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/bug-5218.php'], $errors); + } + + public function testBug5979(): void + { + $this->analyse([__DIR__ . '/data/bug-5979.php'], []); + } + + public function testBug4165(): void + { + $this->analyse([__DIR__ . '/data/bug-4165.php'], []); + } + + public function testBug6053(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6053.php'], []); + } + + public function testBug6438(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6438.php'], []); + } + + public function testBug6589(): void + { + $this->checkUnionTypes = false; + $this->analyse([__DIR__ . '/data/bug-6589.php'], [ + [ + 'Method Bug6589\HelloWorldTemplated::getField() should return TField of Bug6589\Field2 but returns Bug6589\Field.', + 17, + ], + [ + 'Method Bug6589\HelloWorldSimple::getField() should return Bug6589\Field2 but returns Bug6589\Field.', + 31, + ], + ]); + } + + public function testBug6418(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6418.php'], []); + } + + public function testBug6230(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6230.php'], []); + } + + public function testBug5860(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5860.php'], []); + } + + public function testBug6266(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6266.php'], []); + } + + public function testBug6023(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6023.php'], []); + } + + public function testBug5065(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5065.php'], []); + } + + public function testBug5065ExplicitMixed(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5065.php'], [ + [ + 'Method Bug5065\Collection::emptyWorkaround2() should return Bug5065\Collection but returns Bug5065\Collection<(int|string), mixed>.', + 60, + ], + ]); + } + + public function testBug3400(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3400.php'], []); + } + + public function testBug6353(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6353.php'], []); + } + + public function testBug6635Level9(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6635.php'], []); + } + + public function testBug6635Level8(): void + { + $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__ . '/../../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 testBug4163(): void + { + $this->analyse([__DIR__ . '/data/bug-4163.php'], [ + [ + 'Method Bug4163\HelloWorld::lall() should return array but returns array.', + 28, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..8f3161de63 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -0,0 +1,121 @@ + + */ +class StaticMethodCallableRuleTest extends RuleTestCase +{ + + private int $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true); + + return new StaticMethodCallableRule( + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + true, + true, + ), + new PhpVersion($this->phpVersion), + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + + $this->analyse([__DIR__ . '/data/static-method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/static-method-callable.php'], [ + [ + 'Call to static method StaticMethodCallable\Foo::doFoo() with incorrect case: dofoo', + 11, + ], + [ + 'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.', + 12, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Call to an undefined static method StaticMethodCallable\Foo::nonexistent().', + 13, + ], + [ + 'Static call to instance method StaticMethodCallable\Foo::doBar().', + 14, + ], + [ + 'Call to private static method doBar() of class StaticMethodCallable\Bar.', + 15, + ], + [ + 'Cannot call abstract static method StaticMethodCallable\Bar::doBaz().', + 16, + ], + [ + 'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.', + 21, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Cannot call static method doFoo() on int.', + 22, + ], + [ + 'Creating callable from a non-native static method StaticMethodCallable\Lorem::doBar().', + 47, + ], + [ + 'Creating callable from a non-native static method StaticMethodCallable\Ipsum::doBar().', + 66, + ], + ]); + } + + 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/accept-throwable.php b/tests/PHPStan/Rules/Methods/data/accept-throwable.php index ac157cbb2f..aa40e1b99c 100644 --- a/tests/PHPStan/Rules/Methods/data/accept-throwable.php +++ b/tests/PHPStan/Rules/Methods/data/accept-throwable.php @@ -35,7 +35,7 @@ public function doBar(int $i) function () { $foo = new Foo(); try { - + maybeThrows(); } catch (SomeInterface $e) { $foo->doFoo($e); $foo->doBar($e); 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 442cb31759..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 +doBaz(fn () => $this->doFoo()); + } + + public function doBaz(callable $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/arrow-function-returning-void-closure.php b/tests/PHPStan/Rules/Methods/data/arrow-function-returning-void-closure.php new file mode 100644 index 0000000000..3cfb9dc48a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/arrow-function-returning-void-closure.php @@ -0,0 +1,19 @@ + $this->returnVoid(); + } + + public function returnVoid(): void + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10043.php b/tests/PHPStan/Rules/Methods/data/bug-10043.php new file mode 100644 index 0000000000..9980c932ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10043.php @@ -0,0 +1,18 @@ +{$name}; + } +} + +class StdSat extends \stdClass +{ + use WarnDynamicPropertyTrait; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10153.php b/tests/PHPStan/Rules/Methods/data/bug-10153.php new file mode 100644 index 0000000000..e2b5f06840 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10153.php @@ -0,0 +1,28 @@ +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-1656.php b/tests/PHPStan/Rules/Methods/data/bug-1656.php new file mode 100644 index 0000000000..8a66acd00a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1656.php @@ -0,0 +1,16 @@ +test(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-1661.php b/tests/PHPStan/Rules/Methods/data/bug-1661.php new file mode 100644 index 0000000000..ef6b04750a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1661.php @@ -0,0 +1,42 @@ +bar(); + $b = $a->bat(); + $b->someMethod(); + $b->someOtherMethod(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-1903.php b/tests/PHPStan/Rules/Methods/data/bug-1903.php new file mode 100644 index 0000000000..5317a00547 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1903.php @@ -0,0 +1,22 @@ +answersOrder[$qId]) { + return $this->answersOrder[$qId]; + } + + + $this->answersOrder[$qId] = 5; + + return $this->answersOrder[$qId]; + } + +} 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-1971.php b/tests/PHPStan/Rules/Methods/data/bug-1971.php new file mode 100644 index 0000000000..0002418488 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-1971.php @@ -0,0 +1,18 @@ +fallbackDriver) { + return $this->fallbackDriver->getRootIdentifier(); + } + + if (null === $this->rootIdentifier) { + return $this->fallbackDriver->getRootIdentifier(); + } + + return 'foo'; + } + +} + +interface VcsDriver +{ + + public function getRootIdentifier(): string; + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-2573-return.php b/tests/PHPStan/Rules/Methods/data/bug-2573-return.php new file mode 100644 index 0000000000..6b447194e2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-2573-return.php @@ -0,0 +1,32 @@ + $array + * @param array-key $key + * @param D $default + * @return V|D + */ + function idx($array, $key, $default = null) { + if (array_key_exists($key, $array)) { + return $array[$key]; + } + return $default; + } + + /** + * @param array $arr + * @return int + */ + function example($arr) { + return $this->idx($arr, 5, 42); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-2573.php b/tests/PHPStan/Rules/Methods/data/bug-2573.php new file mode 100644 index 0000000000..5d9755fa02 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-2573.php @@ -0,0 +1,19 @@ +make($class); + } + + +} + +trait X { + /** + * @phpstan-template T of object + * @phpstan-param class-string $class + * @phpstan-return T + */ + protected function make(string $class) : object + { + $reflection = new \ReflectionClass($class); + + return $reflection->newInstanceWithoutConstructor(); + } +} + +class HelloWorld2 +{ + + /** + * @phpstan-param class-string $class + */ + public function demo(string $class): void + { + $this->make($class); + } + + /** + * @phpstan-template T of object + * @phpstan-param class-string $class + * @phpstan-return T + */ + protected function make(string $class) : object + { + $reflection = new \ReflectionClass($class); + + return $reflection->newInstanceWithoutConstructor(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3034.php b/tests/PHPStan/Rules/Methods/data/bug-3034.php new file mode 100644 index 0000000000..f693787c09 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3034.php @@ -0,0 +1,22 @@ + + */ +class HelloWorld implements \IteratorAggregate +{ + /** + * @var array + */ + private $list; + + /** + * @return \ArrayIterator + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->list); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3117.php b/tests/PHPStan/Rules/Methods/data/bug-3117.php new file mode 100644 index 0000000000..07670f436b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3117.php @@ -0,0 +1,49 @@ +adjustedWith($this); + } + + /** + * @return static + */ + public function adjustedWith(TemporalAdjuster $adjuster): Temporal + { + return $this; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3118.php b/tests/PHPStan/Rules/Methods/data/bug-3118.php new file mode 100644 index 0000000000..b8d7a930bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3118.php @@ -0,0 +1,58 @@ + $type + */ + public function __construct(string $type) + { + $this->type = $type; + } +} + +abstract class Enum +{ + /** + * @return EnumSet + */ + public static function all(): EnumSet + { + return new EnumSet(static::class); + } +} + +/** + * @extends EnumSet + */ +final class CustomEnumSet extends EnumSet +{ + + public function __construct() + { + parent::__construct(CustomEnum::class); + } +} + +final class CustomEnum extends Enum +{ + public static function all(): EnumSet + { + return new CustomEnumSet(); + } +} + +class CustomEnum2 extends Enum +{ + public static function all(): EnumSet + { + return new CustomEnumSet(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3120.php b/tests/PHPStan/Rules/Methods/data/bug-3120.php new file mode 100644 index 0000000000..41039c47aa --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3120.php @@ -0,0 +1,42 @@ + $myArray + * @return array{type: string, foo: string} + */ + function findByType(array $myArray, string $type): array { + $found = $this->search($myArray, function(array $item) use ($type): bool { + return $item['type'] === $type; + }); + if ($found === null) { + throw new \LogicException(); + } + return $found; + } + +} 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-3400.php b/tests/PHPStan/Rules/Methods/data/bug-3400.php new file mode 100644 index 0000000000..b42066b9c9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3400.php @@ -0,0 +1,34 @@ +values = $values; + } + + /** + * @param class-string $type + * + * @return Collection + * + * @template U of Immutable + */ + public static function ofType(string $type) : self + { + return new self(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3465.php b/tests/PHPStan/Rules/Methods/data/bug-3465.php new file mode 100644 index 0000000000..96cc78936a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3465.php @@ -0,0 +1,20 @@ +setValue(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-3469.php b/tests/PHPStan/Rules/Methods/data/bug-3469.php new file mode 100644 index 0000000000..911d1929c4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3469.php @@ -0,0 +1,29 @@ +doSomething(...$args); +}; + +function (): void { + $args = ['foo', 1]; + if (rand(0, 1)) { + $args[] = 'bar'; + } + + $foo = new Foo(); + $foo->doSomething(...$args); +}; + +function (): void { + $args = ['foo', 1, 'string']; + if (rand(0, 1)) { + $args[0] = 1; + } + + $foo = new Foo(); + $foo->doSomething(...$args); +}; 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 @@ +myRenamedMethod(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3523.php b/tests/PHPStan/Rules/Methods/data/bug-3523.php new file mode 100644 index 0000000000..99d5762446 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3523.php @@ -0,0 +1,44 @@ +> */ + const SYNC_CLASSES = [ + 'a' => A::class, + 'b' => B::class, + 'c' => C::class, + 'd' => D::class, + 'e' => E::class, + 'f' => F::class, + 'g' => G::class, + 'h' => H::class, + 'i' => I::class, + 'j' => J::class, + ]; + + /** @param class-string $class */ + private function getRepository($class) : void + { + } + + public function getSyncEntity(string $type, string $syncId) : void + { + $class = self::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } + + public function getSyncEntity2(string $type, string $syncId) : void + { + $class = static::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } +} + +class HelloWorld2 +{ + const SYNC_CLASSES = [ + 'a' => A::class, + 'b' => B::class, + 'c' => C::class, + 'd' => D::class, + 'e' => E::class, + 'f' => F::class, + 'g' => G::class, + 'h' => H::class, + 'i' => I::class, + 'j' => J::class, + ]; + + /** @param class-string $class */ + private function getRepository($class) : void + { + } + + public function getSyncEntity(string $type, string $syncId) : void + { + $class = self::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } + + public function getSyncEntity2(string $type, string $syncId) : void + { + $class = static::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3534.php b/tests/PHPStan/Rules/Methods/data/bug-3534.php new file mode 100644 index 0000000000..11a9508ad2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3534.php @@ -0,0 +1,22 @@ +terminate()) { + echo "C'est fini"; + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3546.php b/tests/PHPStan/Rules/Methods/data/bug-3546.php new file mode 100644 index 0000000000..939600c450 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3546.php @@ -0,0 +1,40 @@ + */ + private $someInterface; + + /** + * @phpstan-param SomeInterface $someInterface + */ + public function __construct(SomeInterface $someInterface) + { + $this->someInterface = $someInterface; + } +} + +/** + * @phpstan-extends MyAbstractService + */ +class MyService extends MyAbstractService +{ + /** + * @phpstan-param SomeInterface $someInterface + */ + public function __construct(SomeInterface $someInterface) + { + parent::__construct($someInterface); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3555.php b/tests/PHPStan/Rules/Methods/data/bug-3555.php new file mode 100644 index 0000000000..2a877f75b4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3555.php @@ -0,0 +1,31 @@ +run(100); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3580.php b/tests/PHPStan/Rules/Methods/data/bug-3580.php new file mode 100644 index 0000000000..10b703c911 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3580.php @@ -0,0 +1,32 @@ +foo->getClone(); + } +} + + diff --git a/tests/PHPStan/Rules/Methods/data/bug-3629.php b/tests/PHPStan/Rules/Methods/data/bug-3629.php new file mode 100644 index 0000000000..320197ad09 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3629.php @@ -0,0 +1,11 @@ +$method(...$args); + } +} + +function (): void { + Bar::bar(); + Bar::bar(1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-3683.php b/tests/PHPStan/Rules/Methods/data/bug-3683.php new file mode 100644 index 0000000000..34c834dd44 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3683.php @@ -0,0 +1,8 @@ +throw(new \Exception()); + $g->throw(1); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-3723.php b/tests/PHPStan/Rules/Methods/data/bug-3723.php new file mode 100644 index 0000000000..c333cc1524 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3723.php @@ -0,0 +1,18 @@ + $bar The raw frame + * + * @psalm-param array{ + * foo?: Foo::TEST, + * bar: string + * } $bar + */ + public function foo(array $bar): void + { + } +} 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-3958.php b/tests/PHPStan/Rules/Methods/data/bug-3958.php new file mode 100644 index 0000000000..ffb351ba2f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3958.php @@ -0,0 +1,7 @@ + */ + private $otherGenericClass; + + /** @param OtherGenericClass $otherGenericClass */ + public function __construct(OtherGenericClass $otherGenericClass){ + $this->otherGenericClass = $otherGenericClass; + } +} + +/** @extends GenericClass */ +class ChildGenericClass extends GenericClass +{ + /** @param OtherGenericClass $otherGenericClass */ + public function __construct(OtherGenericClass $otherGenericClass){ + parent::__construct($otherGenericClass); + } +} + +/** @template TModlel of BaseModel */ +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-4011.php b/tests/PHPStan/Rules/Methods/data/bug-4011.php new file mode 100644 index 0000000000..6ec3c6cd66 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4011.php @@ -0,0 +1,11 @@ + $className + * @return DoctrineEntityRepository + */ + public function getRepository(string $className): DoctrineEntityRepository; +} + + +/** + * @phpstan-template TEntityClass + * @phpstan-extends DoctrineEntityRepository + */ +interface MyEntityRepositoryInterface extends DoctrineEntityRepository +{ +} + +interface MyEntityManagerInterface extends DoctrineEntityManagerInterface +{ + /** + * @template T + * @param class-string $className + * @return MyEntityRepositoryInterface + */ + public function getRepository(string $className): MyEntityRepositoryInterface; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4017_2.php b/tests/PHPStan/Rules/Methods/data/bug-4017_2.php new file mode 100644 index 0000000000..30adaa35ca --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4017_2.php @@ -0,0 +1,56 @@ + + */ +class Baz extends Bar +{ + + /** + * @param Foo $a + */ + public function doFoo($a) + { + + } + +} + +/** + * @extends Bar<\stdClass> + */ +class Lorem extends Bar +{ + + /** + * @param Foo $a + */ + public function doFoo($a) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4017_3.php b/tests/PHPStan/Rules/Methods/data/bug-4017_3.php new file mode 100644 index 0000000000..f458c85bf2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4017_3.php @@ -0,0 +1,50 @@ + $this->takesShapedArray($x), $items); + array_map(fn ($x) => $this->takesShapedArray($x), $items); + } + + /** + * @param array{0:int,1:string} $x + */ + private function takesShapedArray(array $x): int { return 0; } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4084.php b/tests/PHPStan/Rules/Methods/data/bug-4084.php new file mode 100644 index 0000000000..f246e0e124 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4084.php @@ -0,0 +1,17 @@ + + */ +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-4163.php b/tests/PHPStan/Rules/Methods/data/bug-4163.php new file mode 100644 index 0000000000..7b8de0491c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4163.php @@ -0,0 +1,30 @@ + + */ + function lall() { + $helloCollection = [new HelloWorld(), new HelloWorld()]; + $result = []; + + foreach ($helloCollection as $hello) { + $key = (string)$hello->lall; + + if (!isset($result[$key])) { + $lall = 'do_something_here'; + $result[$key] = $lall; + } + } + + return $result; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4165.php b/tests/PHPStan/Rules/Methods/data/bug-4165.php new file mode 100644 index 0000000000..fd9c4056ab --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4165.php @@ -0,0 +1,36 @@ +client = $client; + } + + /** + * @phpstan-return array<'int'|'stg'|'prd', int> + */ + public function __invoke(): array + { + $result = [ + 'int' => 3, + 'stg' => 4, + 'prd' => 5 + ]; + + $result[$this->client->env()] = 42; + + return $result; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4188.php b/tests/PHPStan/Rules/Methods/data/bug-4188.php new file mode 100644 index 0000000000..8181dbc4a4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4188.php @@ -0,0 +1,32 @@ + $data */ + public function set(array $data): void + { + $this->onlyB(array_filter( + $data, + function ($param): bool { + return $param instanceof B; + }, + )); + } + + /** @param array $data */ + public function setShort(array $data): void + { + $this->onlyB(array_filter( + $data, + fn($param): bool => $param instanceof B, + )); + } + + /** @param B[] $data */ + public function onlyB(array $data): void {} +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4199.php b/tests/PHPStan/Rules/Methods/data/bug-4199.php new file mode 100644 index 0000000000..e9c307c6c8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4199.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug4199; + +class Foo +{ + public function getBar(): ?Bar + { + return null; + } +} + +class Bar +{ + public function getBaz(): Baz + { + return new Baz(); + } + public function getBazOrNull(): ?Baz + { + return null; + } +} + +class Baz +{ + public function answer(): int + { + return 42; + } +} + +function (): void { + $foo = new Foo; + $answer = $foo->getBar()?->getBaz()->answer(); + + $answer2 = $foo->getBar()?->getBazOrNull()->answer(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4211.php b/tests/PHPStan/Rules/Methods/data/bug-4211.php new file mode 100644 index 0000000000..bb8c6270c1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4211.php @@ -0,0 +1,28 @@ +format('j. n. Y'); + } +} + +class HelloWorld +{ + use HelloWorldTraitTest, HelloWorldTrait { + sayHello as hello; + } + + public function sayHello(DateTimeImmutable $date): void { + $this->hello($date); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4214.php b/tests/PHPStan/Rules/Methods/data/bug-4214.php new file mode 100644 index 0000000000..d741ffeb4d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4214.php @@ -0,0 +1,11 @@ +getPrototype(); + return true; + } catch (\ReflectionException $e) { + return false; + } +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4244.php b/tests/PHPStan/Rules/Methods/data/bug-4244.php new file mode 100644 index 0000000000..b5374297f7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4244.php @@ -0,0 +1,6 @@ +value = $value; + } + + final public static function fromString(string $value): self + { + return new static($value); + } +} + +final class ClassB extends ClassC +{ +} + +final class ClassA +{ + public function classB(): ClassB + { + return ClassB::fromString("any"); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4415.php b/tests/PHPStan/Rules/Methods/data/bug-4415.php new file mode 100644 index 0000000000..247de77c3e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4415.php @@ -0,0 +1,96 @@ + + */ +interface CollectionInterface extends \IteratorAggregate +{ + /** + * @param T $item + */ + public function has($item): bool; + + /** + * @return self + */ + public function sort(): self; +} + +/** + * @template T + * @extends CollectionInterface + */ +interface MutableCollectionInterface extends CollectionInterface +{ + /** + * @param T $item + * @phpstan-return self + */ + public function add($item): self; +} + +/** + * @extends CollectionInterface + */ +interface CategoryCollectionInterface extends CollectionInterface +{ + public function has($item): bool; + + /** + * @phpstan-return \Iterator + */ + public function getIterator(): \Iterator; +} + +/** + * @extends MutableCollectionInterface + */ +interface MutableCategoryCollectionInterface extends CategoryCollectionInterface, MutableCollectionInterface +{ +} + +class CategoryCollection implements MutableCategoryCollectionInterface +{ + /** @var array */ + private $categories = []; + + public function add($item): MutableCollectionInterface + { + $this->categories[$item->getName()] = $item; + return $this; + } + + public function has($item): bool + { + return isset($this->categories[$item->getName()]); + } + + public function sort(): CollectionInterface + { + return $this; + } + + public function getIterator(): \Iterator + { + return new \ArrayIterator($this->categories); + } +} + +class Category { + public function getName(): string + { + return ''; + } +} + +function (CategoryCollection $c): void { + foreach ($c as $k => $v) { + assertType('mixed', $k); + assertType(Category::class, $v); + } +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-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-4455-constructor.php b/tests/PHPStan/Rules/Methods/data/bug-4455-constructor.php new file mode 100644 index 0000000000..d5d91501ea --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4455-constructor.php @@ -0,0 +1,19 @@ +nope(); + } + + /** + * @psalm-pure + * @return never + */ + function nope() { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4516.php b/tests/PHPStan/Rules/Methods/data/bug-4516.php new file mode 100644 index 0000000000..df3b34a4c7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4516.php @@ -0,0 +1,20 @@ + $class + */ + public static function valuesOf(string $class): void + { + (new $class())->values(); + assert(method_exists($class, 'values')); + $class::values(); + } + + /** + * @template T + * @param class-string $class + */ + public static function doBar(string $class): void + { + (new $class())->values(); + $class::values(); + } + + /** + * @param class-string $s + */ + public function doBaz(string $s): void + { + $s::valuesOf(\stdClass::class); + $s::valuesOf('Person'); + } + + /** + * @template T of self + * @param class-string $s + */ + public function doLorem(string $s): void + { + $s::valuesOf(\stdClass::class); + $s::valuesOf('Person'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4552.php b/tests/PHPStan/Rules/Methods/data/bug-4552.php new file mode 100644 index 0000000000..f2d5413622 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4552.php @@ -0,0 +1,63 @@ + + */ +class SimpleOptionDefinition implements OptionDefinition { + public function presenter() + { + return new SimpleOptionPresenter(); + } +} + +/** + * @template T of OptionPresenter + * + * @param class-string> $definition + * + * @return T + */ +function present($definition) { + return instantiate($definition)->presenter(); +} + + +/** + * @template T of OptionDefinition + * + * @param class-string $definition + * + * @return T + */ +function instantiate($definition) { + return new $definition; +} + +function (): void { + $p = present(SimpleOptionDefinition::class); + assertType(SimpleOptionPresenter::class, $p); + $p->test(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4590.php b/tests/PHPStan/Rules/Methods/data/bug-4590.php new file mode 100644 index 0000000000..ed2d79d790 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4590.php @@ -0,0 +1,83 @@ +body = $body; + } + + /** + * @phpstan-return T + */ + public function getBody() + { + return $this->body; + } + + /** + * @return static> + */ + public static function testGenericStatic() + { + return new static(["ok" => "hello"]); + } +} + +class Controller +{ + /** + * @return OkResponse> + */ + public function test1(): OkResponse + { + return new OkResponse(["ok" => "hello"]); + } + + /** + * @return OkResponse> + */ + public function test2(): OkResponse + { + return new OkResponse([0 => "hello"]); + } + + /** + * @return OkResponse + */ + public function test3(): OkResponse + { + return new OkResponse(["hello"]); + } + + /** + * @return OkResponse + */ + public function test4(): OkResponse + { + return new OkResponse("hello"); + } + + /** + * @param array $a + * @return OkResponse> + */ + public function test5(array $a): OkResponse + { + return new OkResponse($a); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4603.php b/tests/PHPStan/Rules/Methods/data/bug-4603.php new file mode 100644 index 0000000000..16a1f65272 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4603.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug4603; + +class Foo +{ + + /** + * @param T $val + * + * @return T + * + * @template T + */ + function fcn(mixed $val = null) + { + return $val; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4605.php b/tests/PHPStan/Rules/Methods/data/bug-4605.php new file mode 100644 index 0000000000..c8fd31428a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4605.php @@ -0,0 +1,32 @@ + + * @template-extends ArrayAccess + */ +interface Collection extends \Countable, \IteratorAggregate, \ArrayAccess {} + +class Boo { + /** + * @param Collection $collection + * @return Collection + */ + public function foo(Collection $collection): Collection + { + return $collection; + } + + /** + * @param Collection $collection + * @return Collection + */ + public function boo(Collection $collection): Collection + { + return $collection; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4641.php b/tests/PHPStan/Rules/Methods/data/bug-4641.php new file mode 100644 index 0000000000..e99a071c52 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4641.php @@ -0,0 +1,28 @@ + + * @template U + * @phpstan-param class-string $className + * @phpstan-return T + */ + function getRepository(string $className): IRepository; + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4648.php b/tests/PHPStan/Rules/Methods/data/bug-4648.php new file mode 100644 index 0000000000..b4b9d1c6d2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4648.php @@ -0,0 +1,27 @@ +> */ + public function getChildren(): array; +} + +final class Block implements ParentNodeInterface +{ + /** @var list */ + private $rows = []; + + /** @return list */ + public function getChildren(): array + { + return $this->rows; + } +} + +class Block2 implements ParentNodeInterface +{ + /** @var list */ + private $rows = []; + + /** @return list */ + public function getChildren(): array + { + return $this->rows; + } +} + +/** @implements ChildNodeInterface */ +final class Row implements ChildNodeInterface +{ + /** @var Block $parent */ + private $parent; + + public function getParent(): ParentNodeInterface + { + return $this->parent; + } +} + +/** @implements ChildNodeInterface */ +final class Row2 implements ChildNodeInterface +{ + /** @var Block2 $parent */ + private $parent; + + public function getParent(): ParentNodeInterface + { + return $this->parent; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4707-three.php b/tests/PHPStan/Rules/Methods/data/bug-4707-three.php new file mode 100644 index 0000000000..82b97a0018 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4707-three.php @@ -0,0 +1,26 @@ + */ + public function getChildren(); +} + +/** @implements ChildNodeInterface */ +final class Row implements ChildNodeInterface {} + +class Block implements ParentNodeInterface +{ + /** @return Row */ + public function getChildren() { + return new Row(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4707-two.php b/tests/PHPStan/Rules/Methods/data/bug-4707-two.php new file mode 100644 index 0000000000..c0a2d4f0be --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4707-two.php @@ -0,0 +1,26 @@ + */ + public function getChildren(); +} + +/** @implements ChildNodeInterface */ +final class Row implements ChildNodeInterface {} + +final class Block implements ParentNodeInterface +{ + /** @return Row */ + public function getChildren() { + return new Row(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4707.php b/tests/PHPStan/Rules/Methods/data/bug-4707.php new file mode 100644 index 0000000000..46091671d2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4707.php @@ -0,0 +1,66 @@ +> */ + public function getChildren(): array; +} + +final class Block implements ParentNodeInterface +{ + /** @var list */ + private $rows = []; + + /** @return list */ + public function getChildren(): array + { + return $this->rows; + } +} + +class Block2 implements ParentNodeInterface +{ + /** @var list */ + private $rows = []; + + /** @return list */ + public function getChildren(): array + { + return $this->rows; + } +} + +/** @implements ChildNodeInterface */ +final class Row implements ChildNodeInterface +{ + /** @var Block $parent */ + private $parent; + + public function getParent(): ParentNodeInterface + { + return $this->parent; + } +} + +/** @implements ChildNodeInterface */ +final class Row2 implements ChildNodeInterface +{ + /** @var Block2 $parent */ + private $parent; + + public function getParent(): ParentNodeInterface + { + return $this->parent; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4729.php b/tests/PHPStan/Rules/Methods/data/bug-4729.php new file mode 100644 index 0000000000..001bb88617 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4729.php @@ -0,0 +1,36 @@ + + */ +final class B implements I +{ + function get(): I + { + return $this; + } +} + +/** + * @template T of int + * @implements I + */ +class C implements I +{ + function get(): I + { + return $this; + } +} 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-4795.php b/tests/PHPStan/Rules/Methods/data/bug-4795.php new file mode 100644 index 0000000000..fd7320c30f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4795.php @@ -0,0 +1,47 @@ += 8.0 + +namespace Bug4795; + +abstract class BaseDTO +{ + + final protected static function create(): static + { + $class = static::class; + + return new $class(); + } + + abstract public static function parse(): static; + +} + +final class ConcreteDTO extends BaseDTO +{ + + public string $foo; + + public static function parse(): static + { + $instance = self::create(); + + $instance->foo = 'bar'; + + return $instance; + } +} + +class NonFinalConcreteDTO extends BaseDTO +{ + + public string $foo; + + public static function parse(): static + { + $instance = self::create(); + + $instance->foo = 'bar'; + + return $instance; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4800.php b/tests/PHPStan/Rules/Methods/data/bug-4800.php new file mode 100644 index 0000000000..d4f725d9e4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4800.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug4800; + +class HelloWorld +{ + /** + * @param string|int ...$arguments + */ + public function a(string $bar = '', ...$arguments): string + { + return ''; + } + + public function b(): void + { + $this->a(bar: 'baz', foo: 'bar', c: 3); + $this->a(foo: 'bar', c: 3); + } +} + +class HelloWorld2 +{ + /** + * @param string|int ...$arguments + */ + public function a(string $bar, ...$arguments): string + { + return ''; + } + + public function b(): void + { + $this->a(bar: 'baz', foo: 'bar', c: 3); + $this->a(foo: 'baz', bar: 'bar', c: 3); + $this->a(foo: 'bar', c: 3); + } +} 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-4844.php b/tests/PHPStan/Rules/Methods/data/bug-4844.php new file mode 100644 index 0000000000..34725b4eed --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4844.php @@ -0,0 +1,33 @@ +update_attributes([ + $name => $value, + ]); + } + + /** + * Updates as an array given attributes and saves the record. + * + * @param non-empty-array $attributes + * + * @return bool + */ + public function update_attributes($attributes) + { + return true; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4854.php b/tests/PHPStan/Rules/Methods/data/bug-4854.php new file mode 100644 index 0000000000..17a590c626 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4854.php @@ -0,0 +1,66 @@ + + * @extends \ArrayAccess + */ +interface DomainsAvailabilityInterface extends \IteratorAggregate, \ArrayAccess +{ + public const AVAILABLE = 1; + public const UNAVAILABLE = 2; + public const UNKNOWN = 3; +} + +abstract class AbstractDomainsAvailability implements DomainsAvailabilityInterface +{ + /** + * @var int[] + */ + protected array $domains; + + /** + * {@inheritdoc} + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->domains); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->domains[] = $value; + } else { + $this->domains[$offset] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset): bool + { + return isset($this->domains[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset): void + { + unset($this->domains[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset): int + { + return $this->domains[$offset]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4886.php b/tests/PHPStan/Rules/Methods/data/bug-4886.php new file mode 100644 index 0000000000..b29e43c9b0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4886.php @@ -0,0 +1,31 @@ + $b; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5065.php b/tests/PHPStan/Rules/Methods/data/bug-5065.php new file mode 100644 index 0000000000..b3af840a3a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5065.php @@ -0,0 +1,62 @@ + + */ + private $source; + + /** + * @param callable(): iterable $callable + */ + public function __construct(callable $callable) + { + $this->source = $callable; + } + + /** + * @template NewTKey of array-key + * @template NewT + * + * @return self + */ + public static function empty(): self + { + return new self(static fn(): iterable => []); + } + + /** + * @template NewTKey of array-key + * @template NewT + * + * @return self + */ + public static function emptyWorkaround(): self + { + /** @var array $empty */ + $empty = []; + + return new self(static fn() => $empty); + } + + /** + * @template NewTKey of array-key + * @template NewT + * + * @return self + */ + public static function emptyWorkaround2(): self + { + /** @var Closure(): iterable */ + $func = static fn(): iterable => []; + + return new self($func); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5089.php b/tests/PHPStan/Rules/Methods/data/bug-5089.php new file mode 100644 index 0000000000..eb5306ece8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5089.php @@ -0,0 +1,21 @@ +encode('foo')); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5218.php b/tests/PHPStan/Rules/Methods/data/bug-5218.php new file mode 100644 index 0000000000..0c2022ce5c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5218.php @@ -0,0 +1,29 @@ + + */ +final class IA implements \IteratorAggregate +{ + /** @var array */ + private $data = []; + + public function getIterator() : \Traversable { + return new \ArrayIterator($this->data); + } +} + +class Foo +{ + + /** + * @return mixed + */ + public function doFoo() + { + return 1; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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 @@ +clientPool); + + do { + $client = next($this->clientPool); + + if (false === $client) { + $client = reset($this->clientPool); + + if (false === $client) { + throw new \Exception(); + } + } + + // Case when there is only one and the last one has been disabled + if ($last === $client && $client->isDisabled()) { + throw new \Exception(); + } + } while ($client->isDisabled()); + + return $client; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5258.php b/tests/PHPStan/Rules/Methods/data/bug-5258.php new file mode 100644 index 0000000000..27a751f859 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5258.php @@ -0,0 +1,23 @@ +method2($params); + + if (!empty($params['other_key'])) $this->method2($params); + } + + /** + * @param array{other_key:string} $params + **/ + public function method2(array$params): void + { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5259.php b/tests/PHPStan/Rules/Methods/data/bug-5259.php new file mode 100644 index 0000000000..de650ccc0b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5259.php @@ -0,0 +1,15 @@ +date = $date instanceof \DateTimeImmutable ? $date : \DateTimeImmutable::createFromMutable($date); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5372.php b/tests/PHPStan/Rules/Methods/data/bug-5372.php new file mode 100644 index 0000000000..712516416c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5372.php @@ -0,0 +1,88 @@ + */ + private $values; + + /** + * @param array $values + */ + public function __construct(array $values) + { + $this->values = $values; + } + + /** + * @template V + * + * @param callable(T): V $callback + * + * @return self + */ + public function map(callable $callback): self { + return new self(array_map($callback, $this->values)); + } + + /** + * @template V of string + * + * @param callable(T): V $callback + * + * @return self + */ + public function map2(callable $callback): self { + return new self(array_map($callback, $this->values)); + } +} + +class Foo +{ + + /** @param Collection $list */ + function takesStrings(Collection $list): void { + echo serialize($list); + } + + /** @param class-string $classString */ + public function doFoo(string $classString) + { + $col = new Collection(['foo', 'bar']); + assertType('Bug5372\Collection', $col); + + $newCol = $col->map(static fn(string $var): string => $var . 'bar'); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + + $newCol = $col->map(static fn(string $var): string => $classString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + + $newCol = $col->map2(static fn(string $var): string => $classString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + } + + /** @param literal-string $literalString */ + public function doBar(string $literalString) + { + $col = new Collection(['foo', 'bar']); + $newCol = $col->map(static fn(string $var): string => $literalString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + + $newCol = $col->map2(static fn(string $var): string => $literalString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5436.php b/tests/PHPStan/Rules/Methods/data/bug-5436.php new file mode 100644 index 0000000000..a79fa83492 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5436.php @@ -0,0 +1,12 @@ +setOptions(["timeout" => 1]); + + $request->getBody()->append(""); + + $client = new \http\Client(); + $client->enqueue($request)->send(); + + $response = $client->getResponse($request); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-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-5536.php b/tests/PHPStan/Rules/Methods/data/bug-5536.php new file mode 100644 index 0000000000..18c305a9e3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5536.php @@ -0,0 +1,47 @@ + + */ +class Model +{ + /** + * @param array $args + */ + public function __call(string $method, array $args) + { + return $this->$method; + } + + /** + * @param array $args + */ + public static function __callStatic(string $method, array $args) + { + return (new static)->$method(...$args); + } +} + +/** + * @template TModel of Model + */ +class Builder +{ + /** + * @return array + */ + public function all(): array + { + return []; + } +} + +class User extends Model {} + +function (): void { + User::all(); + $user = new User(); + $user->all(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-5562.php b/tests/PHPStan/Rules/Methods/data/bug-5562.php new file mode 100644 index 0000000000..3bcf4b1d4e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5562.php @@ -0,0 +1,34 @@ +bar($test); + assertType('T of int|string (method Bug5562\Foo::foo(), argument)', $bar); + + return $bar; + } + + /** + * @template T of int|string + * @param T $test + * @return T + */ + public function bar($test) + { + return $test; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5591.php b/tests/PHPStan/Rules/Methods/data/bug-5591.php new file mode 100644 index 0000000000..b8510d13ee --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5591.php @@ -0,0 +1,46 @@ + The fully-qualified (::class) class name of the entity being managed. */ + protected $entityClass; + + /** @param TEntity|null $record */ + public function outerMethod($record = null): void + { + $record = $this->innerMethod($record); + } + + /** + * @param TEntity|null $record + * + * @return TEntity + */ + public function innerMethod($record = null): object + { + $class = $this->entityClass; + return new $class(); + } +} + +/** + * @template TEntity as EntityA|EntityB + * @extends TestClass + */ +class TestClass2 extends TestClass +{ + public function outerMethod($record = null): void + { + $record = $this->innerMethod($record); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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-5749.php b/tests/PHPStan/Rules/Methods/data/bug-5749.php new file mode 100644 index 0000000000..4ca314c953 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5749.php @@ -0,0 +1,50 @@ +|null', $type); + + if ($type) { + assertType('non-empty-array', $type); + $typeSql = ' AND type IN ' . self::dbarray_int($type) . ' '; + } else { + assertType('0|array{}|null', $type); + $typeSql = ''; + } + + // ... + + return $typeSql; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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 new file mode 100644 index 0000000000..d0b84715f0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5757.php @@ -0,0 +1,29 @@ + $iterable + * @phpstan-return iterable> + */ + public static function chunk(iterable $iterable, int $chunkSize): iterable + { + return []; + } +} + +class Foo +{ + + public function doFoo() + { + assertType('iterable>', Helper::chunk([1], 3)); + assertType('iterable>', Helper::chunk([], 3)); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-577.php b/tests/PHPStan/Rules/Methods/data/bug-577.php new file mode 100644 index 0000000000..25077f01bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-577.php @@ -0,0 +1,18 @@ + 1) { + static::step2($foo); + } + } + + public static function step2(int $bar): void + { + var_dump($bar); + } +} 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 @@ += 8.0 + +namespace Bug5868; + +class HelloWorld +{ + public function nullable1(): ?self + { + // OK + $tmp = $this->nullable1()?->nullable1()?->nullable2(); + $tmp = $this->nullable1()?->nullable3()->nullable2()?->nullable3()->nullable1(); + + // Error + $tmp = $this->nullable1()->nullable1()?->nullable2(); + $tmp = $this->nullable1()?->nullable1()->nullable2(); + $tmp = $this->nullable1()?->nullable3()->nullable2()->nullable3()->nullable1(); + + return $this->nullable1()?->nullable3(); + } + + public function nullable2(): ?self + { + return $this; + } + + public function nullable3(): self + { + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5869.php b/tests/PHPStan/Rules/Methods/data/bug-5869.php new file mode 100644 index 0000000000..43fd5607c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5869.php @@ -0,0 +1,32 @@ +sayHello($class); + } + + /** + * @param T $class + */ + public function sayHello(BaseInterface $class): void + { + echo 'Hello', PHP_EOL; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5893.php b/tests/PHPStan/Rules/Methods/data/bug-5893.php new file mode 100644 index 0000000000..f1666d75fe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5893.php @@ -0,0 +1,31 @@ + + */ + public static function getClass($object) + { + return get_class($object); + } +} + +/** + * @phpstan-template T of object + */ +class Foo { + /** @phpstan-param T $object */ + public function foo(object $object): string { + if (method_exists($object, '__toString') && null !== $object->__toString()) { + return $object->__toString(); + } + + return Test::getClass($object); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5898.php b/tests/PHPStan/Rules/Methods/data/bug-5898.php new file mode 100644 index 0000000000..cbf5e3cb47 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5898.php @@ -0,0 +1,25 @@ + + */ + public function dataProviderForTestValidCommands(): array + { + $data = [ + // left out some commands here for simplicity ... + // [...] + [ + 'migrations:execute', + SplQueue::class, + ], + ]; + + // this is only available with DBAL 2.x + if (class_exists(ImportCommand::class)) { + $data[] = [ + 'dbal:import', + ImportCommand::class, + ]; + } + + return $data; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6023.php b/tests/PHPStan/Rules/Methods/data/bug-6023.php new file mode 100644 index 0000000000..59628e4bac --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6023.php @@ -0,0 +1,22 @@ +, leftovers: array} $groups + * @return array{commissions: array, leftovers: array} + */ + public function groupByType(array $groups, Clearable $clearable): array + { + $group = $clearable->type === 'foo' ? 'commissions' : 'leftovers'; + $groups[$group][] = $clearable; + return $groups; + } +} + +class Clearable +{ + public string $type = 'foo'; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6053.php b/tests/PHPStan/Rules/Methods/data/bug-6053.php new file mode 100644 index 0000000000..0a2de15614 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6053.php @@ -0,0 +1,29 @@ + + */ + public function processItems($items): ?array + { + if ($items === null || !is_array($items)) { + return null; + } + + if ($items === []) { + return []; + } + + $result = []; + foreach ($items as $item) { + $result[] = 'something'; + } + + return $result; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6055.php b/tests/PHPStan/Rules/Methods/data/bug-6055.php new file mode 100644 index 0000000000..027a9486ec --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6055.php @@ -0,0 +1,23 @@ +check(null); + + $this->check(array_merge( + ['key1' => true], + ['key2' => 'value'] + )); + } + + /** + * @param ?array $items + */ + private function check(?array $items): void + { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6081.php b/tests/PHPStan/Rules/Methods/data/bug-6081.php new file mode 100644 index 0000000000..aff80084e1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6081.php @@ -0,0 +1,19 @@ +something($array); + if(count($array) !== 0){ + $this->something($array); + } + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6104.php b/tests/PHPStan/Rules/Methods/data/bug-6104.php new file mode 100644 index 0000000000..319a1fdd5f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6104.php @@ -0,0 +1,36 @@ + + */ +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-6118.php b/tests/PHPStan/Rules/Methods/data/bug-6118.php new file mode 100644 index 0000000000..8b5d81a8fc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6118.php @@ -0,0 +1,68 @@ += 8.0 + +namespace Bug6118; + + +/** + * @template-covariant T of mixed + */ +class Element { + /** @var T */ + public mixed $value; + + /** + * @param Element $element + */ + function getValue(Element $element) : void { + } + + /** + * @param Element $element + */ + function takesValue(Element $element) : void { + $this->getValue($element); + } + + + /** + * @param Element $element + */ + function getValue2(Element $element) : void { + } + + /** + * @param Element $element + */ + function takesValue2(Element $element) : void { + getValue2($element); + } +} + +/** + * @template-covariant T of string|int|array + */ +interface Example { + /** + * @return T|null + */ + public function normalize(): string|int|array|null; +} + +/** + * @implements Example> + */ +class Foo implements Example { + public function normalize(): int + { + return 0; + } + /** + * @param Example> $example + */ + function foo(Example $example): void { + } + + function bar(): void { + $this->foo(new Foo()); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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-6230.php b/tests/PHPStan/Rules/Methods/data/bug-6230.php new file mode 100644 index 0000000000..64e24a052f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6230.php @@ -0,0 +1,43 @@ + $it + * @return ?iterable + */ + function test($it): ?iterable + { + return $it; + } + +} + +/** + * @template T + */ +class Example +{ + /** + * @var ?iterable + */ + private $input; + + + /** + * @param iterable $input + */ + public function __construct(iterable $input) + { + $this->input = $input; + } + + /** @return ?iterable */ + public function get(): ?iterable + { + return $this->input; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6236.php b/tests/PHPStan/Rules/Methods/data/bug-6236.php new file mode 100644 index 0000000000..167be4353a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6236.php @@ -0,0 +1,27 @@ + $t + */ + public static function sayHello(\Traversable $t): void + { + } + + /** + * @param \SplObjectStorage<\DateTime, \DateTime> $foo + */ + public function doFoo($foo) + { + $this->sayHello(new \ArrayIterator([new \DateTime()])); + + $this->sayHello(new \ArrayIterator(['a' => new \DateTime()])); + + $this->sayHello($foo); + } +} + diff --git a/tests/PHPStan/Rules/Methods/data/bug-6249.php b/tests/PHPStan/Rules/Methods/data/bug-6249.php new file mode 100644 index 0000000000..7d2db1ed3e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6249.php @@ -0,0 +1,136 @@ + + */ +interface CollectionInterface extends Countable, IteratorAggregate +{ +} + +namespace Bug6249N2; + +use ArrayIterator; +use InvalidArgumentException; +use IteratorIterator; +use Bug6249N1\EntityInterface; +use Traversable; +/** + * @extends \IteratorIterator> + */ +final class Eii extends IteratorIterator +{ + /** + * @param iterable $iterable + */ + public function __construct(iterable $iterable) + { + parent::__construct($iterable instanceof Traversable ? $iterable : new ArrayIterator($iterable)); + } + + /** + * @return EntityInterface + */ + public function current() + { + $current = parent::current(); + + if (!$current instanceof EntityInterface) { + throw new InvalidArgumentException(sprintf('Item "%s" must be an instance of "%s".', gettype($current), EntityInterface::class)); + } + + return $current; + } + + /** + * return ?string + */ + public function key() + { + if ($this->valid()) { + /** @var EntityInterface $current */ + $current = $this->current(); + + return $current->getId(); + } + + return null; + } +} + +namespace Bug6249N3; + +use ArrayIterator; +use Countable; +use Bug6249N1\CollectionInterface; +use Traversable; + +/** + * @template TKey of array-key + * @template T + * @implements CollectionInterface + */ +final class Cw implements CollectionInterface +{ + /** + * @var iterable + */ + private iterable $iterable; + + /** + * @param iterable $iterable + */ + private function __construct(iterable $iterable) + { + $this->iterable = $iterable; + } + + /** + * @param iterable $iterable + * + * @return self + */ + public static function fromIterable(iterable $iterable): self + { + return new self($iterable); + } + + public function count(): int + { + if (is_array($this->iterable) || $this->iterable instanceof Countable) { + return count($this->iterable); + } + + return count(iterator_to_array($this->iterable, false)); + } + + public function getIterator(): Traversable + { + if (is_array($this->iterable)) { + return new ArrayIterator($this->iterable); + } + + return $this->iterable; + } +} + +class Foo +{ + + public function doFoo() + { + \Bug6249N3\Cw::fromIterable(new \Bug6249N2\Eii([])); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6264.php b/tests/PHPStan/Rules/Methods/data/bug-6264.php new file mode 100644 index 0000000000..f8f10ca26a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6264.php @@ -0,0 +1,42 @@ +doFooImpl(); + } +} + +class FooBar extends Bar +{ + use SpecificFoo; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6266.php b/tests/PHPStan/Rules/Methods/data/bug-6266.php new file mode 100644 index 0000000000..058e18eec3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6266.php @@ -0,0 +1,27 @@ += 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-6306.php b/tests/PHPStan/Rules/Methods/data/bug-6306.php new file mode 100644 index 0000000000..8dce4adabb --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6306.php @@ -0,0 +1,21 @@ +myNumber(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6353.php b/tests/PHPStan/Rules/Methods/data/bug-6353.php new file mode 100644 index 0000000000..f9542a4c15 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6353.php @@ -0,0 +1,69 @@ + + */ + function some($t): Option { + /** @implements Option */ + return new class($t) implements Option { + /** + * @param T $t + */ + public function __construct(private $t) { + } + + /** + * @return T + */ + public function get() { + return $this->t; + } + }; + } + + /** + * @return Option + */ + function none(): Option { + /** @implements Option */ + return new class() implements Option { + + /** + * @return never + */ + public function get() { + throw new \Exception(); + } + }; + } + /** + * @template T + * @param callable():T $fn + * @return Option + */ + function fromCallbackThatCanThrow(callable $fn) { + try { + $a = $this->some($fn()); + } catch (\Throwable $failure) { + $a = $this->none(); + } + return $a; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-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-6418.php b/tests/PHPStan/Rules/Methods/data/bug-6418.php new file mode 100644 index 0000000000..d1fb02a4fe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6418.php @@ -0,0 +1,19 @@ + $foos + */ + function doFoo(?array $foos = null): void {} + + /** + * @return list + */ + function doBar(): array + { + return [ + 'hello', + 'world', + ]; + } + + function doBaz() + { + $this->doFoo([ + 'foo', + 'bar', + ...$this->doBar(), + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6438.php b/tests/PHPStan/Rules/Methods/data/bug-6438.php new file mode 100644 index 0000000000..826725d665 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6438.php @@ -0,0 +1,44 @@ + $value, 'description' => $description]; + } + + /** + * @phpstan-return array{value: int, description: string}|null + */ + public function testInteger() + { + return $this->getValueDescription(5, 'Description'); + } + + /** + * @phpstan-return array{value: bool, description: string}|null + */ + public function testBooleanTrue() + { + return $this->getValueDescription(true, 'Description'); + } + + /** + * @phpstan-return array{value: bool, description: string}|null + */ + public function testBooleanFalse() + { + return $this->getValueDescription(false, 'Description'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6462.php b/tests/PHPStan/Rules/Methods/data/bug-6462.php new file mode 100644 index 0000000000..4717ce76c4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6462.php @@ -0,0 +1,35 @@ + $g */ + public function foo(\Generator $g): void; +} + +class Bar +{ + + function test(Foo $foo): void { + $foo->foo((function(string $str) { + yield $str; + })('hello')); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6472.php b/tests/PHPStan/Rules/Methods/data/bug-6472.php new file mode 100644 index 0000000000..db2ca978a7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6472.php @@ -0,0 +1,29 @@ +|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 new file mode 100644 index 0000000000..5fc2439152 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php @@ -0,0 +1,110 @@ += 8.1 + +namespace CallMethodInEnum; + +enum Foo +{ + + public function doFoo() + { + $this->doFoo(); + $this->doNonexistent(); + } + +} + +trait FooTrait +{ + + public function doFoo() + { + $this->doFoo(); + $this->doNonexistent(); + } + +} + +enum Bar +{ + + use FooTrait; + +} + +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 c488ac3e25..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'); } @@ -1477,10 +1477,10 @@ class ClassStringWithUpperBounds * @template T of \Exception * @param class-string $s * @param T $object + * @return T */ public function doFoo(string $s, $object) { - } public function doBar(\Throwable $t) @@ -1640,3 +1640,218 @@ public function doFoo(array $params) } } + +class NumericStringParam +{ + + /** + * @param numeric-string $test + */ + public function sayHello(string $test): void + { + + } + + public function doFoo() + { + $this->sayHello(123); + $this->sayHello('abc'); + $this->sayHello('123'); + } + +} + +class XmlReaderOpen +{ + + public function doFoo(\XMLReader $xml): void + { + $xml->open('http://', null); + } + + public function openStatically(): void + { + $xml = \XMLReader::open('http://', null); + if ($xml !== false) { + $xml->read(); + } + } + +} + +class HelloWorld +{ + /** + * @param \DateTime|\DateTimeImmutable|int $date + */ + public function sayHello($date): void + { + } + + /** + * @param \DateTimeInterface|int $d + */ + public function foo($d): void + { + $this->sayHello($d); + } +} + +class HelloWorld2 +{ + /** + * @param \DateTime|\DateTimeImmutable $date + */ + public function sayHello($date): void + { + } + + /** + * @param \DateTimeInterface $d + */ + public function foo($d): void + { + $this->sayHello($d); + } +} + +class HelloWorld3 +{ + /** + * @param array<\DateTime|\DateTimeImmutable>|int $date + */ + public function sayHello($date): void + { + } + + /** + * @param \DateTimeInterface $d + */ + public function foo($d): void + { + $this->sayHello($d); + } +} + +class InvalidReturnTypeUsingArrayTemplateTypeBound +{ + + /** + * @template T of array + * @param T $a + * @return T + */ + function bar(array $a): array + { + return $a; + } + + public function doBar() + { + $this->bar(range(1, 3)); + } + +} + +class KeyOfParam +{ + public const JFK = 'jfk'; + public const LGA = 'lga'; + + private const ALL = [ + self::JFK => 'John F. Kennedy Airport', + self::LGA => 'La Guardia Airport', + ]; + + /** + * @param key-of $code + */ + public function foo(string $code): void + { + } + + public function test(): void + { + $this->foo(KeyOfParam::JFK); + $this->foo('jfk'); + $this->foo('sfo'); + } +} + +class ValueOfParam +{ + public const JFK = 'jfk'; + public const LGA = 'lga'; + + public const ALL = [ + self::JFK => 'John F. Kennedy Airport', + self::LGA => 'La Guardia Airport', + ]; + + /** + * @param value-of $code + */ + public function foo(string $code): void + { + } + + public function test(): void + { + $this->foo(ValueOfParam::ALL[ValueOfParam::JFK]); + $this->foo('John F. Kennedy Airport'); + $this->foo('Newark Liberty International'); + } +} + +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-private-method-static.php b/tests/PHPStan/Rules/Methods/data/call-private-method-static.php new file mode 100644 index 0000000000..f273d48d73 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-private-method-static.php @@ -0,0 +1,37 @@ += 8.0 + +namespace 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 96e370b430..0fadae62c3 100644 --- a/tests/PHPStan/Rules/Methods/data/call-static-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-static-methods.php @@ -329,3 +329,27 @@ public function doBar(TraitWithStaticMethod $a): void } } + +class CallWithStatic +{ + + private function doFoo() + { + + } + + public function doBar() + { + static::doFoo(); // reported by different rule + static::nonexistent(); + } + +} + +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/calling-method-with-phpDocs-implicit-inheritance.php b/tests/PHPStan/Rules/Methods/data/calling-method-with-phpDocs-implicit-inheritance.php index 7bd1002b47..fb4e457417 100644 --- a/tests/PHPStan/Rules/Methods/data/calling-method-with-phpDocs-implicit-inheritance.php +++ b/tests/PHPStan/Rules/Methods/data/calling-method-with-phpDocs-implicit-inheritance.php @@ -135,9 +135,9 @@ function (TestArrayObject2 $arrayObject2): void { class TestArrayObject3 extends \ArrayObject { - public function append($someValue) + public function append($someValue): void { - return parent::append($someValue); + parent::append($someValue); } } diff --git a/tests/PHPStan/Rules/Methods/data/check-explicit-mixed.php b/tests/PHPStan/Rules/Methods/data/check-explicit-mixed.php index 7d50d87121..9cf96eacce 100644 --- a/tests/PHPStan/Rules/Methods/data/check-explicit-mixed.php +++ b/tests/PHPStan/Rules/Methods/data/check-explicit-mixed.php @@ -1,4 +1,4 @@ -= 8.0 namespace CheckExplicitMixedMethodCall; @@ -67,3 +67,89 @@ public function doLorem($t): void } } + +class TemplateMixed +{ + + /** + * @template T + * @param T $t + */ + public function doFoo($t): void + { + $this->doBar($t); + } + + /** + * @param mixed $mixed + */ + public function doBar($mixed): void + { + $this->doFoo($mixed); + } + +} + +class CallableMixed +{ + + /** + * @param callable(mixed): void $cb + */ + public function doFoo(callable $cb): void + { + + } + + /** + * @param callable(int): void $cb + */ + public function doBar(callable $cb): void + { + + } + + /** + * @param callable(): mixed $cb + */ + public function doFoo2(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public function doBar2(callable $cb): void + { + + } + + public function doLorem(int $i, mixed $m): void + { + $acceptsInt = function (int $i): void { + + }; + $this->doFoo($acceptsInt); + $this->doBar($acceptsInt); + + $acceptsMixed = function (mixed $m): void { + + }; + $this->doFoo($acceptsMixed); + $this->doBar($acceptsMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + $this->doFoo2($returnsInt); + $this->doBar2($returnsInt); + + $returnsMixed = function () use ($m): mixed { + return $m; + }; + $this->doFoo2($returnsMixed); + $this->doBar2($returnsMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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 e4dec44d16..57b9ccf5ff 100644 --- a/tests/PHPStan/Rules/Methods/data/closure-bind.php +++ b/tests/PHPStan/Rules/Methods/data/closure-bind.php @@ -33,6 +33,11 @@ public function fooMethod(): Foo $foo->nonexistentMethod(); }, null, new Foo()); + \Closure::bind(function (Foo $foo) { + $foo->privateMethod(); + $foo->nonexistentMethod(); + }, null, get_class(new Foo())); + \Closure::bind(function () { // $this is Foo $this->privateMethod(); @@ -44,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 @@ + $foo + */ + public function doFoo(array $foo): void + { + + } + + /** @param array $bars */ + public function doBar(array $bars): void + { + + } + + /** + * @param non-empty-array $a + */ + public function doBaz(array $a): void + { + + } + +} + +/** @template T */ +class Bar +{ + +} diff --git a/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php b/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php new file mode 100644 index 0000000000..a2c881155b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php @@ -0,0 +1,15 @@ += 8.0 + +namespace DefaultValueForPromotedProperty; + +class Foo +{ + + public function __construct( + private int $foo = 'foo', + /** @var int */ private $foo = '', + private int $baz = 1, + private int $intProp = null, + ) {} + +} diff --git a/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php new file mode 100644 index 0000000000..174fef244d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php @@ -0,0 +1,35 @@ += 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/disallow-named-arguments.php b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments.php new file mode 100644 index 0000000000..daff44a02b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments.php @@ -0,0 +1,18 @@ +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/enums-typehints.php b/tests/PHPStan/Rules/Methods/data/enums-typehints.php new file mode 100644 index 0000000000..0ce822e3f3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/enums-typehints.php @@ -0,0 +1,13 @@ + */ +interface IteratorChild2 extends \Iterator +{ + + /** @return int */ + #[\ReturnTypeWillChange] + public function key(); + + /** @return int */ + #[\ReturnTypeWillChange] + public function current(); + +} + +class Foo +{ + + public function doFoo(IteratorChild $c) + { + foreach ($c as $k => $v) { + assertType('int', $k); + assertType('int', $v); + } + } + + public function doFoo2(IteratorChild2 $c) + { + foreach ($c as $k => $v) { + assertType('mixed', $k); + assertType('mixed', $v); + } + } + +} + +interface IteratorChild3 extends \Iterator +{ + +} + +class IteratorChildTest +{ + + public function doFoo(IteratorChild3 $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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 @@ += 8.1 + +namespace FirstClassCallableMethodWithoutSideEffect; + +class Foo +{ + + public function doFoo(): void + { + $f = $this->doFoo(...); + + $this->doFoo(...); + } + +} + +class Bar +{ + + function doFoo(): never + { + throw new \Exception(); + } + + /** + * @throws \Exception + */ + function doBar() + { + throw new \Exception(); + } + + function doBaz(): void + { + $f = $this->doFoo(...); + $this->doFoo(...); + + $g = $this->doBar(...); + $this->doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-callable-static-method-without-side-effect.php b/tests/PHPStan/Rules/Methods/data/first-class-callable-static-method-without-side-effect.php new file mode 100644 index 0000000000..b387cd8be0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-callable-static-method-without-side-effect.php @@ -0,0 +1,42 @@ += 8.1 + +namespace FirstClassCallableStaticMethodWithoutSideEffect; + +class Foo +{ + + public static function doFoo(): void + { + $f = self::doFoo(...); + + self::doFoo(...); + } + +} + +class Bar +{ + + static function doFoo(): never + { + throw new \Exception(); + } + + /** + * @throws \Exception + */ + static function doBar() + { + throw new \Exception(); + } + + function doBaz(): void + { + $f = self::doFoo(...); + self::doFoo(...); + + $g = self::doBar(...); + self::doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-method-callable.php b/tests/PHPStan/Rules/Methods/data/first-class-method-callable.php new file mode 100644 index 0000000000..c969b924c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-method-callable.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FirstClassMethodCallable; + +class Foo +{ + + public function doFoo(int $i): void + { + $this->doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-static-method-callable.php b/tests/PHPStan/Rules/Methods/data/first-class-static-method-callable.php new file mode 100644 index 0000000000..755e9be311 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-static-method-callable.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FirstClassStaticMethodCallable; + +class Foo +{ + + public static function doFoo(int $i): void + { + self::doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/generic-return-type-never.php b/tests/PHPStan/Rules/Methods/data/generic-return-type-never.php new file mode 100644 index 0000000000..5e66fb0c36 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generic-return-type-never.php @@ -0,0 +1,76 @@ + + */ + public function doBazBaz($p) + { + + } + + public function doTest(Ipsum $ipsum): void + { + $this->doFoo($ipsum); // OK + $this->doBar($ipsum); // OK + $this->doBar(new Sit()); // error + $this->doBaz(); // OK + $this->doBazBaz($ipsum); // OK + $this->doBazBaz(new Sit()); // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/generic-variance.php b/tests/PHPStan/Rules/Methods/data/generic-variance.php new file mode 100644 index 0000000000..5a9ce6dcad --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generic-variance.php @@ -0,0 +1,99 @@ + $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/generics-empty-array.php b/tests/PHPStan/Rules/Methods/data/generics-empty-array.php new file mode 100644 index 0000000000..3eec6a01da --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generics-empty-array.php @@ -0,0 +1,25 @@ + $a + * @return array{TKey, T} + */ + public function doFoo(array $a = []): array + { + + } + + public function doBar() + { + $this->doFoo(); + $this->doFoo([]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/generics-infer-collection.php b/tests/PHPStan/Rules/Methods/data/generics-infer-collection.php new file mode 100644 index 0000000000..8c17507d70 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generics-infer-collection.php @@ -0,0 +1,76 @@ + $items + */ + public function __construct(array $items = []) + { + + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection2 +{ + + public function __construct(array $items = []) + { + + } + +} + +class Foo +{ + + public function doFoo() + { + $this->doBar(new ArrayCollection()); + $this->doBar(new ArrayCollection([])); + $this->doBar(new ArrayCollection(['foo', 'bar'])); + } + + /** + * @param ArrayCollection $c + * @return void + */ + public function doBar(ArrayCollection $c) + { + + } + +} + +class Bar +{ + + public function doFoo() + { + $this->doBar(new ArrayCollection2()); + $this->doBar(new ArrayCollection2([])); + $this->doBar(new ArrayCollection2(['foo', 'bar'])); + } + + /** + * @param ArrayCollection2 $c + * @return void + */ + public function doBar(ArrayCollection2 $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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 @@ + + */ +class Foo implements \IteratorAggregate +{ + + /** @var \stdClass[] */ + private $items; + + #[\ReturnTypeWillChange] + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('(int|string)', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Bar implements \IteratorAggregate +{ + + /** @var array */ + private $items; + + #[\ReturnTypeWillChange] + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('int', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Baz implements \IteratorAggregate +{ + + /** @var array */ + private $items; + + #[\ReturnTypeWillChange] + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('string', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Lorem implements \IteratorAggregate +{ + + /** @var array<\stdClass> */ + private $items; + + #[\ReturnTypeWillChange] + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('(int|string)', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Ipsum implements \IteratorAggregate +{ + + /** @var array */ + private $items; + + #[\ReturnTypeWillChange] + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('int|string', $it->key()); + + return $it; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/intersection-types.php b/tests/PHPStan/Rules/Methods/data/intersection-types.php new file mode 100644 index 0000000000..a7b11a5e28 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/intersection-types.php @@ -0,0 +1,43 @@ += 8.1 + +namespace MethodIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +class FooClass +{ + + public function doFoo(Foo&Bar $a): Foo&Bar + { + + } + + public function doBar(Lorem&Ipsum $a): Lorem&Ipsum + { + + } + + public function doBaz(int&mixed $a): int&mixed + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/less-parameters-variadics.php b/tests/PHPStan/Rules/Methods/data/less-parameters-variadics.php new file mode 100644 index 0000000000..47a9d75205 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/less-parameters-variadics.php @@ -0,0 +1,43 @@ + */ + 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/literal-string.php b/tests/PHPStan/Rules/Methods/data/literal-string.php new file mode 100644 index 0000000000..e919270eb0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/literal-string.php @@ -0,0 +1,70 @@ +requireLiteralString($string); + $this->requireLiteralString($literalString); + $this->requireLiteralString('foo'); + $this->requireLiteralString($int); + $this->requireLiteralString(1); + + $mixed = doFoo(); + $this->requireLiteralString($mixed); + } + + /** + * @param literal-string $s + */ + public function requireLiteralString(string $s): void + { + + } + + /** + * @param array $a + */ + public function requireArrayOfLiteralStrings(array $a): void + { + + } + + /** + * @param mixed $mixed + * @param array $arrayOfStrings + * @param array $arrayOfLiteralStrings + * @param array $arrayOfMixed + */ + public function doBar( + $mixed, + array $arrayOfStrings, + array $arrayOfLiteralStrings, + array $arrayOfMixed + ): void + { + $this->requireArrayOfLiteralStrings($mixed); + $this->requireArrayOfLiteralStrings($arrayOfStrings); + $this->requireArrayOfLiteralStrings($arrayOfLiteralStrings); + $this->requireArrayOfLiteralStrings($arrayOfMixed); + } + + public function doGet(): void + { + $this->requireLiteralString($_GET); + $this->requireLiteralString($_GET['x']); + $this->requireLiteralString($_GET['x']['y']); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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 @@ += 8.0 + +namespace MatchExprVoidUsed; + +class Foo +{ + + public function doFoo($m): void + { + match ($this->doLorem()) { + $this->doBar() => $this->doBaz(), + default => $this->doBaz(), + }; + } + + public function doBar(): void + { + + } + + public function doBaz(): void + { + + } + + public function doLorem(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/memcache-pool-get.php b/tests/PHPStan/Rules/Methods/data/memcache-pool-get.php new file mode 100644 index 0000000000..711560b787 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/memcache-pool-get.php @@ -0,0 +1,13 @@ +regular('test'); + (new Bzz())->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-call-statement-no-side-effects.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects.php index b677e7c4bf..b3f45e7684 100644 --- a/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects.php +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects.php @@ -16,4 +16,53 @@ public function doBar(\DateTimeImmutable $dti) $dti->createFromFormat('Y-m-d', '2019-07-24'); } + public function doBaz(\Exception $e) + { + $e->getCode(); + } + +} + +class Bar +{ + + public function doFoo() + { + + } + + /** + * @phpstan-pure + */ + public function doPure() + { + + } + + /** + * @phpstan-pure + * @throws void + */ + public function doPureWithThrowsVoid() + { + + } + + /** + * @phpstan-pure + * @throws \Exception + */ + public function doPureWithThrowsException() + { + + } + + public function doBar(): void + { + $this->doFoo(); + $this->doPure(); // report + $this->doPureWithThrowsVoid(); // report + $this->doPureWithThrowsException(); // do not report + } + } diff --git a/tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php b/tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php new file mode 100644 index 0000000000..3ad95fe40a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-callable-not-supported.php @@ -0,0 +1,13 @@ += 8.1 + +namespace MethodCallableNotSupported; + +class Foo +{ + + public function doFoo(): void + { + $this->doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-callable.php b/tests/PHPStan/Rules/Methods/data/method-callable.php new file mode 100644 index 0000000000..b86bca74e0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-callable.php @@ -0,0 +1,88 @@ += 8.1 + +namespace MethodCallable; + +class Foo +{ + + public function doFoo(int $i): void + { + $this->doFoo(...); + $this->dofoo(...); + $this->doNonexistent(...); + $i->doFoo(...); + } + + public function doBar(Bar $bar): void + { + $bar->doBar(...); + } + + public function doBaz(Nonexistent $n): void + { + $n->doFoo(...); + } + +} + +class Bar +{ + + private function doBar() + { + + } + +} + +class ParentClass +{ + + private function doFoo() + { + + } + +} + +class ChildClass extends ParentClass +{ + + public function doBar() + { + $this->doFoo(...); + } + +} + +/** + * @method void doBar() + */ +class Lorem +{ + + public function doFoo() + { + $this->doBar(...); + } + + public function __call($name, $arguments) + { + + } + + +} + +/** + * @method void doBar() + */ +class Ipsum +{ + + public function doFoo() + { + $this->doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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/misleadingTypehints.php b/tests/PHPStan/Rules/Methods/data/misleadingTypehints.php index 4948b6b6a3..ef1c3b6c42 100644 --- a/tests/PHPStan/Rules/Methods/data/misleadingTypehints.php +++ b/tests/PHPStan/Rules/Methods/data/misleadingTypehints.php @@ -3,7 +3,7 @@ class FooWithoutNamespace { - public function misleadingBoolReturnType(): boolean + public function misleadingBoolReturnType(): \boolean { if (rand(0, 1)) { return true; @@ -18,7 +18,7 @@ public function misleadingBoolReturnType(): boolean } } - public function misleadingIntReturnType(): integer + public function misleadingIntReturnType(): \integer { if (rand(0, 1)) { return 1; diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-impl-enum.php b/tests/PHPStan/Rules/Methods/data/missing-method-impl-enum.php new file mode 100644 index 0000000000..28f00b429e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-method-impl-enum.php @@ -0,0 +1,24 @@ + $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 146f6848de..480373825a 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php @@ -92,3 +92,56 @@ public function returnsGenericClass(): GenericClass } } + +class CallableSignature +{ + + public function doFoo(): callable + { + + } + +} + +class IterableIntersection +{ + + /** @return FooInterface[]|\Traversable */ + 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/missing-typehint-promoted-properties.php b/tests/PHPStan/Rules/Methods/data/missing-typehint-promoted-properties.php new file mode 100644 index 0000000000..179da04b12 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-typehint-promoted-properties.php @@ -0,0 +1,26 @@ += 8.0 + +namespace MissingTypehintPromotedProperties; + +class Foo +{ + + public function __construct( + private array $foo, + /** @var array */private array $bar + ) { } + +} + +class Bar +{ + + /** + * @param array $bar + */ + public function __construct( + private array $foo, + private array $bar + ) { } + +} diff --git a/tests/PHPStan/Rules/Methods/data/mixin.php b/tests/PHPStan/Rules/Methods/data/mixin.php index 3c8695cf34..1c8af21196 100644 --- a/tests/PHPStan/Rules/Methods/data/mixin.php +++ b/tests/PHPStan/Rules/Methods/data/mixin.php @@ -41,7 +41,7 @@ function (Baz $baz): void { }; /** - * @template T + * @template T of object * @mixin T */ class GenericFoo diff --git a/tests/PHPStan/Rules/Methods/data/named-arguments.php b/tests/PHPStan/Rules/Methods/data/named-arguments.php new file mode 100644 index 0000000000..25d9ef362b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/named-arguments.php @@ -0,0 +1,98 @@ +doFoo( + i: 1, + 2, + 3 + ); + $this->doFoo( + 1, + i: 1, + j: 2, + k: 3 + ); + $this->doFoo( + i: 1, + i: 2, + j: 3, + k: 4 + ); + + $this->doFoo( + 1, + j: 3 + ); + + $this->doFoo( + 1, + 2, + 3, + z: 4 + ); + + $this->doFoo( + 'foo', + j: 2, + k: 3 + ); + + $this->doFoo( + 1, + j: 'foo', + k: 3 + ); + + } + + public function doBaz(&$i): void + { + + } + + public function doLorem(?\stdClass $foo): void + { + $this->doBaz(i: 1); + $this->doBaz(i: $foo?->bar); + + $this->doFoo(i: 1, ...['j' => 2, 'k' => 3]); + + $this->doFoo(...['k' => 3, 'i' => 1, 'j' => 'str']); + + $this->doFoo(...['k' => 3, 'i' => 1, 'str']); + } + + public function doIpsum(int $a, int $b, string ...$args): void + { + + } + + public function doDolor(): void + { + $this->doIpsum(...[1, 2, 3, 'foo' => 'foo']); + $this->doIpsum(...[1, 2, 'foo' => 'foo']); + $this->doIpsum(...['a' => 1, 'b' => 2, 'foo' => 'foo']); + $this->doIpsum(...['a' => 1, 'b' => 'foo', 'foo' => 'foo']); + $this->doIpsum(...['a' => 1, 'b' => 'foo', 'foo' => 1]); + $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/native-union-types.php b/tests/PHPStan/Rules/Methods/data/native-union-types.php new file mode 100644 index 0000000000..2721ec711e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/native-union-types.php @@ -0,0 +1,18 @@ += 8.0 + +namespace NativeUnionTypesSupport; + +class Foo +{ + + public function doFoo(int|bool $foo): int|bool + { + return 1; + } + + public function doBar(): int|bool + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/new-in-initializers.php b/tests/PHPStan/Rules/Methods/data/new-in-initializers.php new file mode 100644 index 0000000000..5369835557 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/new-in-initializers.php @@ -0,0 +1,16 @@ += 8.1 + +namespace MethodNewInInitializers; + +class Foo +{ + + /** + * @param int $i + */ + public function doFoo($i = new \stdClass(), object $o = new \stdClass()) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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/non-empty-string-verbosity.php b/tests/PHPStan/Rules/Methods/data/non-empty-string-verbosity.php new file mode 100644 index 0000000000..3437ea767e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/non-empty-string-verbosity.php @@ -0,0 +1,21 @@ +doBar($s); + } + + public function doBar(int $i): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call-rule.php b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call-rule.php new file mode 100644 index 0000000000..8484424f09 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call-rule.php @@ -0,0 +1,20 @@ += 8.0 + +namespace NullsafeMethodCallRule; + +class Foo +{ + + public function doFoo( + $mixed, + ?\Exception $nullable, + \Exception $nonNullable + ): void + { + $mixed?->doFoo(); + $nullable?->doFoo(); + $nonNullable?->doFoo(); + (null)?->doFoo(); // reported by a different rule + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call-statement-no-side-effects.php b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call-statement-no-side-effects.php new file mode 100644 index 0000000000..9400ad283c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call-statement-no-side-effects.php @@ -0,0 +1,13 @@ += 8.0 + +namespace NullsafeMethodCallNoSideEffects; + +class Foo +{ + + public function doFoo(?\Exception $e): void + { + $e?->getMessage(); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php new file mode 100644 index 0000000000..0eb89b8ae8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php @@ -0,0 +1,37 @@ += 8.0 + +namespace NullsafeMethodCall; + +class Foo +{ + + public function doFoo(?self $selfOrNull): void + { + $selfOrNull?->doBar(); + $selfOrNull?->doBar(1); + } + + public function doBar(): void + { + + } + + public function doBaz(&$passedByRef): void + { + + } + + public function doLorem(?self $selfOrNull): void + { + $this->doBaz($selfOrNull?->test); + $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/only-relevant-unable-to-resolve-template-type.php b/tests/PHPStan/Rules/Methods/data/only-relevant-unable-to-resolve-template-type.php new file mode 100644 index 0000000000..2b9b0dadfc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/only-relevant-unable-to-resolve-template-type.php @@ -0,0 +1,59 @@ +doFoo(1); + $this->doBar(); + $this->doBaz(1); + } + + /** + * @template T + * @param mixed $a + * @return T + */ + public function doIpsum($a) + { + + } + + public function doDolor() + { + $this->doIpsum(1); + } + +} 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-method.php b/tests/PHPStan/Rules/Methods/data/overriding-method.php index 961da3439b..cce1427a8a 100644 --- a/tests/PHPStan/Rules/Methods/data/overriding-method.php +++ b/tests/PHPStan/Rules/Methods/data/overriding-method.php @@ -306,3 +306,13 @@ public function doFoo() } } + +class FixedArrayOffsetExists extends \SplFixedArray +{ + + public function offsetExists(int $index) + { + + } + +} 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/overriding-variadics.php b/tests/PHPStan/Rules/Methods/data/overriding-variadics.php new file mode 100644 index 0000000000..b91bb38579 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-variadics.php @@ -0,0 +1,69 @@ += 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/readonly-property-passed-by-reference.php b/tests/PHPStan/Rules/Methods/data/readonly-property-passed-by-reference.php new file mode 100644 index 0000000000..d53cf4c345 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/readonly-property-passed-by-reference.php @@ -0,0 +1,24 @@ += 8.1 + +namespace ReadonlyPropertyPassedByRef; + +class Foo +{ + + private int $foo; + + private readonly int $bar; + + public function doFoo() + { + $this->doBar($this->foo); + $this->doBar($this->bar); + $this->doBar(param: $this->bar); + } + + public function doBar(&$param): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/rector-do-while-var-issue.php b/tests/PHPStan/Rules/Methods/data/rector-do-while-var-issue.php new file mode 100644 index 0000000000..e30eb8336d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/rector-do-while-var-issue.php @@ -0,0 +1,39 @@ +processCharacterClass($cls); + } else { + $cls = 'foo'; + } + } while (doFoo()); + } + + public function doFoo2(string $cls): void + { + do { + if (doBar()) { + [$cls] = $this->processCharacterClass($cls); + } else { + $cls = 'foo'; + } + } while (doFoo()); + } + + /** + * @return int[]|string[] + */ + private function processCharacterClass(string $cls): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/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 new file mode 100644 index 0000000000..d93cdd1bd8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php @@ -0,0 +1,52 @@ + 8.0 + +namespace RequiredAfterOptional; + +class Foo +{ + + public function doFoo($foo = null, $bar): void // not OK + { + + } + + public function doBar(int $foo = null, $bar): void // is OK + { + } + + public function doBaz(int $foo = 1, $bar): void // not OK + { + } + + 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-static.php b/tests/PHPStan/Rules/Methods/data/return-static.php new file mode 100644 index 0000000000..fa7d6b6b90 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-static.php @@ -0,0 +1,30 @@ +returnStatic(); + } +} + +final class B2 extends A +{ + /** @return static */ + public function test() + { + return $this->returnStatic(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/return-template-union.php b/tests/PHPStan/Rules/Methods/data/return-template-union.php new file mode 100644 index 0000000000..a154bfd03d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-template-union.php @@ -0,0 +1,38 @@ += 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 8e8ff8407f..e7030f8ad8 100644 --- a/tests/PHPStan/Rules/Methods/data/returnTypes.php +++ b/tests/PHPStan/Rules/Methods/data/returnTypes.php @@ -349,7 +349,7 @@ public function returnsNullInTernary(): int } } - public function misleadingBoolReturnType(): boolean + public function misleadingBoolReturnType(): \ReturnTypes\boolean { if (rand(0, 1)) { return true; @@ -362,7 +362,7 @@ public function misleadingBoolReturnType(): boolean } } - public function misleadingIntReturnType(): integer + public function misleadingIntReturnType(): \ReturnTypes\integer { if (rand(0, 1)) { return 1; @@ -375,7 +375,7 @@ public function misleadingIntReturnType(): 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 @@ -571,6 +571,7 @@ public function test() class Collection implements \IteratorAggregate { + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator([]); @@ -581,6 +582,7 @@ public function getIterator() class AnotherCollection implements \IteratorAggregate { + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator([]); @@ -747,7 +749,7 @@ public function __wakeup() return 1; } - public function __set_state(array $properties) + public static function __set_state(array $properties) { return ['foo' => 'bar']; } @@ -831,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; } @@ -1184,7 +1185,7 @@ public function doBar(\DateTimeInterface $date): \DateTimeImmutable } /** - * @template CollectionKey + * @template CollectionKey of array-key * @template CollectionValue * @implements \Iterator */ @@ -1206,6 +1207,7 @@ public function add($value, $key = null): void /** * @return CollectionKey|null */ + #[\ReturnTypeWillChange] public function key() { return key($this->data); @@ -1226,3 +1228,55 @@ public function getIterable(): iterable } } + +class NeverReturn +{ + + /** + * @return never + */ + public function doFoo(): void + { + return; + } + + /** + * @return never + */ + public function doBaz3(): string + { + try { + throw new \Exception('try'); + } catch (\Exception $e) { + throw new \Exception('catch'); + } finally { + return 'finally'; + } + } + +} + +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 @@ += 8.1 + +namespace StaticMethodCallableNotSupported; + +class Foo +{ + + public static function doFoo(): void + { + self::doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/static-method-callable.php b/tests/PHPStan/Rules/Methods/data/static-method-callable.php new file mode 100644 index 0000000000..d8256c1c4b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-method-callable.php @@ -0,0 +1,69 @@ += 8.1 + +namespace StaticMethodCallable; + +class Foo +{ + + public static function doFoo() + { + self::doFoo(...); + self::dofoo(...); + Nonexistent::doFoo(...); + self::nonexistent(...); + self::doBar(...); + Bar::doBar(...); + Bar::doBaz(...); + } + + public function doBar(Nonexistent $n, int $i) + { + $n::doFoo(...); + $i::doFoo(...); + } + +} + +abstract class Bar +{ + + private static function doBar() + { + + } + + abstract public static function doBaz(); + +} + +/** + * @method static void doBar() + */ +class Lorem +{ + + public function doFoo() + { + self::doBar(...); + } + + public function __call($name, $arguments) + { + + } + + +} + +/** + * @method static void doBar() + */ +class Ipsum +{ + + public function doFoo() + { + self::doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/static-method-named-arguments.php b/tests/PHPStan/Rules/Methods/data/static-method-named-arguments.php new file mode 100644 index 0000000000..4046b4b1c0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-method-named-arguments.php @@ -0,0 +1,19 @@ +doFoo(new Bar()); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/stringable.php b/tests/PHPStan/Rules/Methods/data/stringable.php new file mode 100644 index 0000000000..cc689ea4ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/stringable.php @@ -0,0 +1,53 @@ +doFoo(new Foo()); + $this->doFoo(new Bar()); + $this->doFoo($l); + $this->doBaz($l); + } + + public function doBaz(string $s): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/tagged-unions.php b/tests/PHPStan/Rules/Methods/data/tagged-unions.php new file mode 100644 index 0000000000..1f76221d5a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/tagged-unions.php @@ -0,0 +1,14 @@ + false, 'id' => 5]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/template-string-bound.php b/tests/PHPStan/Rules/Methods/data/template-string-bound.php new file mode 100644 index 0000000000..c55ae9c885 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/template-string-bound.php @@ -0,0 +1,53 @@ +value = $value; + } + + /** + * @return T + */ + public function getValue(): string + { + return $this->value; + } + +} + +/** @template T of int */ +class Bar +{ + + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function getValue(): int + { + return $this->value; + } + +} 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 new file mode 100644 index 0000000000..0471f4a578 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php @@ -0,0 +1,108 @@ + + */ + #[\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/typehints.php b/tests/PHPStan/Rules/Methods/data/typehints.php index 8a328a7b14..f967e44906 100644 --- a/tests/PHPStan/Rules/Methods/data/typehints.php +++ b/tests/PHPStan/Rules/Methods/data/typehints.php @@ -105,3 +105,40 @@ function unknownTypesInArrays(array $array) } } + +class CallableTypehints +{ + + /** @param callable(Bla): Ble $cb */ + public function doFoo(callable $cb): void + { + + } + +} + +/** + * @template T + */ +class TemplateTypeMissingInParameter +{ + + /** + * @template U of object + * @param class-string $class + */ + public function doFoo(string $class): void + { + + } + + /** + * @template U of object + * @param class-string $class + */ + public function doBar(string $class): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/unable-to-resolve-callback-parameter-type.php b/tests/PHPStan/Rules/Methods/data/unable-to-resolve-callback-parameter-type.php new file mode 100644 index 0000000000..6678a89a55 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/unable-to-resolve-callback-parameter-type.php @@ -0,0 +1,65 @@ +callback = $callback; + } + + /** + * Returns a string representation of the constraint. + */ + public function toString(): string + { + return 'is accepted by specified callback'; + } + + /** + * Evaluates the constraint for parameter $value. Returns true if the + * constraint is met, false otherwise. + * + * @param CallbackInput $other + */ + protected function matches($other): bool + { + return ($this->callback)($other); + } +} + + +class Foo +{ + + /** + * @template CallbackInput of mixed + * @param callable(CallbackInput $callback): bool $callback + * @return Callback + */ + public function callback(callable $callback): Callback + { + return new Callback($callback); + } + + public function test(): void + { + $cb = $this->callback(function (int $i): bool { + return true; + }); + assertType(Callback::class . '', $cb); + } + +} 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/MissingClosureNativeReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Missing/MissingClosureNativeReturnTypehintRuleTest.php deleted file mode 100644 index 65dd141daa..0000000000 --- a/tests/PHPStan/Rules/Missing/MissingClosureNativeReturnTypehintRuleTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -class MissingClosureNativeReturnTypehintRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new MissingClosureNativeReturnTypehintRule(true); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/missing-closure-native-return-typehint.php'], [ - [ - 'Anonymous function should have native return typehint "void".', - 10, - ], - [ - 'Anonymous function should have native return typehint "void".', - 13, - ], - [ - 'Anonymous function should have native return typehint "Generator".', - 16, - ], - [ - 'Mixing returning values with empty return statements - return null should be used here.', - 25, - ], - [ - 'Anonymous function should have native return typehint "?int".', - 23, - ], - [ - 'Anonymous function should have native return typehint "?int".', - 33, - ], - [ - 'Anonymous function sometimes return something but return statement at the end is missing.', - 40, - ], - [ - 'Anonymous function should have native return typehint "array".', - 46, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index 755dbda529..23bb2c73cb 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -4,19 +4,21 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MissingReturnRuleTest extends RuleTestCase { - /** @var bool */ - private $checkExplicitMixedMissingReturn; + private bool $checkExplicitMixedMissingReturn; + + private bool $checkPhpDocMissingReturn = true; protected function getRule(): Rule { - return new MissingReturnRule($this->checkExplicitMixedMissingReturn, true); + return new MissingReturnRule($this->checkExplicitMixedMissingReturn, $this->checkPhpDocMissingReturn); } public function testRule(): void @@ -30,7 +32,7 @@ public function testRule(): void ], [ 'Method MissingReturn\Foo::doBar() should return int but return statement is missing.', - 16, + 15, ], [ 'Method MissingReturn\Foo::doBaz() should return int but return statement is missing.', @@ -38,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.', @@ -96,6 +102,26 @@ public function testRule(): void 'Method MissingReturn\MissingReturnGenerators::bodySpecifiedTReturn() should return string but return statement is missing.', 370, ], + [ + 'Method MissingReturn\NeverReturn::doBaz() should always throw an exception or terminate script execution but doesn\'t do that.', + 473, + ], + [ + '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, + ], ]); } @@ -127,4 +153,242 @@ 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; + + require_once __DIR__ . '/data/bug-3669.php'; + $this->analyse([__DIR__ . '/data/bug-3669.php'], []); + } + + public function dataCheckPhpDocMissingReturn(): array + { + return [ + [ + true, + [ + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo() should return int|string but return statement is missing.', + 11, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo2() should return string|null but return statement is missing.', + 19, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo3() should return int|string but return statement is missing.', + 29, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo4() should return string|null but return statement is missing.', + 39, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo5() should return mixed but return statement is missing.', + 49, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo() should return int|string but return statement is missing.', + 59, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo2() should return string|null but return statement is missing.', + 64, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo3() should return int|string but return statement is missing.', + 71, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo4() should return string|null but return statement is missing.', + 78, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo5() should return mixed but return statement is missing.', + 85, + ], + ], + ], + [ + false, + [ + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo() should return int|string but return statement is missing.', + 11, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo3() should return int|string but return statement is missing.', + 29, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo5() should return mixed but return statement is missing.', + 49, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo() should return int|string but return statement is missing.', + 59, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo2() should return string|null but return statement is missing.', + 64, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo3() should return int|string but return statement is missing.', + 71, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo4() should return string|null but return statement is missing.', + 78, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo5() should return mixed but return statement is missing.', + 85, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataCheckPhpDocMissingReturn + * @param list $errors + */ + public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, array $errors): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixedMissingReturn = true; + $this->checkPhpDocMissingReturn = $checkPhpDocMissingReturn; + $this->analyse([__DIR__ . '/data/check-phpdoc-missing-return.php'], $errors); + } + + public function dataModelMixin(): array + { + return [ + [ + true, + ], + [ + false, + ], + ]; + } + + /** + * @dataProvider dataModelMixin + */ + public function testModelMixin(bool $checkExplicitMixedMissingReturn): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; + $this->checkPhpDocMissingReturn = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/model-mixin.php'], [ + [ + 'Method ModelMixin\Model::__callStatic() should return mixed but return statement is missing.', + 13, + ], + ]); + } + + 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-2682.php b/tests/PHPStan/Rules/Missing/data/bug-2682.php new file mode 100644 index 0000000000..61e97a5292 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-2682.php @@ -0,0 +1,15 @@ += 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-3669.php b/tests/PHPStan/Rules/Missing/data/bug-3669.php new file mode 100644 index 0000000000..b9c6e8ee9e --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-3669.php @@ -0,0 +1,15 @@ += 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.0 + +namespace CheckPhpDocMissingReturn; + +class Foo +{ + + /** + * @return string|int + */ + public function doFoo() + { + + } + + /** + * @return string|null + */ + public function doFoo2() + { + + } + + /** + * @return string|int + */ + public function doFoo3() + { + if (rand()) { + return 'foo'; + } + } + + /** + * @return string|null + */ + public function doFoo4() + { + if (rand()) { + return 'foo'; + } + } + + /** + * @return mixed + */ + public function doFoo5() + { + if (rand()) { + return 'foo'; + } + } + +} + +class Bar +{ + + public function doFoo(): string|int + { + + } + + public function doFoo2(): ?string + { + + } + + public function doFoo3(): string|int + { + if (rand()) { + return 'foo'; + } + } + + public function doFoo4(): ?string + { + if (rand()) { + return 'foo'; + } + } + + public function doFoo5(): mixed + { + if (rand()) { + return 'foo'; + } + } + +} diff --git a/tests/PHPStan/Rules/Missing/data/missing-closure-native-return-typehint.php b/tests/PHPStan/Rules/Missing/data/missing-closure-native-return-typehint.php deleted file mode 100644 index 1eb7d37d14..0000000000 --- a/tests/PHPStan/Rules/Missing/data/missing-closure-native-return-typehint.php +++ /dev/null @@ -1,55 +0,0 @@ - 'bar', - ]; - - return $array; - }; - } - -} diff --git a/tests/PHPStan/Rules/Missing/data/missing-return.php b/tests/PHPStan/Rules/Missing/data/missing-return.php index 9feb1062f1..31ef820be6 100644 --- a/tests/PHPStan/Rules/Missing/data/missing-return.php +++ b/tests/PHPStan/Rules/Missing/data/missing-return.php @@ -232,7 +232,7 @@ public function doBar(): int public function doBaz(): int { try { - return 1; + maybeThrow(); return 1; } catch (\Exception $e) { return 1; } catch (\Throwable $e) { @@ -396,6 +396,43 @@ public function bodySpecifiedVoidTReturn3(): \Generator return 2; } + public function yieldInWhileCondition(): \Generator + { + while($foo = yield 'foo') { + } + } + + public function yieldInForCondition(): \Generator + { + for($foo = 0; $foo > 0; $foo = yield 1) { + } + } + + public function yieldInDoCondition(): \Generator + { + do { + } while(yield 1); + } + + public function yieldInForeach(): \Generator + { + foreach (yield 1 as $bar) { + } + } + + public function yieldInIf(): \Generator + { + if (yield 1) { + } + } + + public function yieldInSwitch(): \Generator + { + switch (yield 1) { + default: + } + } + } class VoidUnion @@ -410,3 +447,99 @@ public function doFoo() } } + +class NeverReturn +{ + + /** + * @return never + */ + public function doFoo() + { + throw new \Exception(); + } + + /** + * @return never + */ + public function doBar() + { + die; + } + + /** + * @return never + */ + public function doBaz() + { + + } + + /** + * @return never + */ + public function doBaz2(): array + { + + } + +} + +class ClosureWithMissingReturnWithoutTypehint +{ + + public function doFoo(): void + { + function () { + if (rand(0, 1)) { + return; + } + }; + + function () { + if (rand(0, 1)) { + return null; + } + }; + } + +} + +class MorePreciseMissingReturnLines +{ + + public function doFoo(): int + { + if (doFoo()) { + echo 1; + } elseif (doBar()) { + + } else { + return 1; + } + } + + public function doFoo2(): int + { + if (doFoo()) { + return 1; + } elseif (doBar()) { + return 2; + } + } + +} + +class AnonymousFunctionOnlySometimesThrowsException +{ + + public function doFoo(): void + { + $cb = function (): void { + if (rand(0, 1)) { + throw new \Exception('bad luck'); + } + }; + } + +} diff --git a/tests/PHPStan/Rules/Missing/data/property-hooks-missing-return.php b/tests/PHPStan/Rules/Missing/data/property-hooks-missing-return.php new file mode 100644 index 0000000000..eff880e46c --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/property-hooks-missing-return.php @@ -0,0 +1,34 @@ += 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 @@ + + * @extends RuleTestCase */ -class ExistingNamesInGroupUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingNamesInGroupUseRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingNamesInGroupUseRule($broker, new ClassCaseSensitivityCheck($broker), true); + $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 @@ -27,6 +41,7 @@ public function testRule(): void [ 'Used function Uses\baz not found.', 7, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Interface Uses\Lorem referenced with incorrect case: Uses\LOREM.', @@ -39,6 +54,7 @@ public function testRule(): void [ 'Used constant Uses\OTHER_CONSTANT not found.', 15, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); } diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index e70e70037f..13fa5a254b 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php @@ -3,17 +3,31 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingNamesInUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingNamesInUseRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingNamesInUseRule($broker, new ClassCaseSensitivityCheck($broker), true); + $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 @@ -23,10 +37,12 @@ public function testRule(): void [ 'Used function Uses\bar not found.', 7, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Used constant Uses\OTHER_CONSTANT not found.', 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Function Uses\foo used with incorrect case: Uses\Foo.', @@ -36,6 +52,23 @@ public function testRule(): void 'Interface Uses\Lorem referenced with incorrect case: Uses\LOREM.', 10, ], + [ + 'Class DateTime referenced with incorrect case: DATETIME.', + 11, + ], + ]); + } + + 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/Namespaces/data/uses.php b/tests/PHPStan/Rules/Namespaces/data/uses.php index 2a941fa9f9..192a46e6af 100644 --- a/tests/PHPStan/Rules/Namespaces/data/uses.php +++ b/tests/PHPStan/Rules/Namespaces/data/uses.php @@ -8,3 +8,4 @@ use const Uses\MY_CONSTANT, Uses\OTHER_CONSTANT; use function Uses\Foo; use Uses\LOREM; +use DATETIME; diff --git a/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php new file mode 100644 index 0000000000..8dfd60fc26 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/InvalidAssignVarRuleTest.php @@ -0,0 +1,62 @@ + + */ +class InvalidAssignVarRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidAssignVarRule(new NullsafeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-assign-var.php'], [ + [ + 'Nullsafe operator cannot be on left side of assignment.', + 12, + ], + [ + 'Nullsafe operator cannot be on left side of assignment.', + 13, + ], + [ + 'Nullsafe operator cannot be on left side of assignment.', + 14, + ], + [ + 'Nullsafe operator cannot be on left side of assignment.', + 16, + ], + [ + 'Nullsafe operator cannot be on left side of assignment.', + 17, + ], + [ + 'Expression on left side of assignment is not assignable.', + 31, + ], + [ + 'Expression on left side of assignment is not assignable.', + 33, + ], + [ + 'Nullsafe operator cannot be on right side of assignment by reference.', + 39, + ], + [ + 'Nullsafe operator cannot be on right side of assignment by reference.', + 40, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index df74c7cbca..cc27629dcf 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -2,19 +2,28 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\Printer\Printer; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidBinaryOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidBinaryOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule { return new InvalidBinaryOperationRule( - new \PhpParser\PrettyPrinter\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), ); } @@ -98,17 +107,709 @@ 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, ], [ 'Binary operation "+" between stdClass and int results in an error.', 157, ], + [ + 'Binary operation "+" between non-empty-string and 10 results in an error.', + 184, + ], + [ + 'Binary operation "-" between non-empty-string and 10 results in an error.', + 185, + ], + [ + 'Binary operation "*" between non-empty-string and 10 results in an error.', + 186, + ], + [ + 'Binary operation "/" between non-empty-string and 10 results in an error.', + 187, + ], + [ + 'Binary operation "+" between 10 and non-empty-string results in an error.', + 189, + ], + [ + 'Binary operation "-" between 10 and non-empty-string results in an error.', + 190, + ], + [ + 'Binary operation "*" between 10 and non-empty-string results in an error.', + 191, + ], + [ + 'Binary operation "/" between 10 and non-empty-string results in an error.', + 192, + ], + [ + 'Binary operation "+" between string and 10 results in an error.', + 194, + ], + [ + 'Binary operation "-" between string and 10 results in an error.', + 195, + ], + [ + 'Binary operation "*" between string and 10 results in an error.', + 196, + ], + [ + 'Binary operation "/" between string and 10 results in an error.', + 197, + ], + [ + 'Binary operation "+" between 10 and string results in an error.', + 199, + ], + [ + 'Binary operation "-" between 10 and string results in an error.', + 200, + ], + [ + 'Binary operation "*" between 10 and string results in an error.', + 201, + ], + [ + 'Binary operation "/" between 10 and string results in an error.', + 202, + ], + [ + 'Binary operation "+" between class-string and 10 results in an error.', + 204, + ], + [ + 'Binary operation "-" between class-string and 10 results in an error.', + 205, + ], + [ + 'Binary operation "*" between class-string and 10 results in an error.', + 206, + ], + [ + 'Binary operation "/" between class-string and 10 results in an error.', + 207, + ], + [ + 'Binary operation "+" between 10 and class-string results in an error.', + 209, + ], + [ + 'Binary operation "-" between 10 and class-string results in an error.', + 210, + ], + [ + 'Binary operation "*" between 10 and class-string results in an error.', + 211, + ], + [ + 'Binary operation "/" between 10 and class-string results in an error.', + 212, + ], + [ + 'Binary operation "+" between literal-string and 10 results in an error.', + 214, + ], + [ + 'Binary operation "-" between literal-string and 10 results in an error.', + 215, + ], + [ + 'Binary operation "*" between literal-string and 10 results in an error.', + 216, + ], + [ + 'Binary operation "/" between literal-string and 10 results in an error.', + 217, + ], + [ + 'Binary operation "+" between 10 and literal-string results in an error.', + 219, + ], + [ + 'Binary operation "-" between 10 and literal-string results in an error.', + 220, + ], + [ + 'Binary operation "*" between 10 and literal-string results in an error.', + 221, + ], + [ + 'Binary operation "/" between 10 and literal-string results in an error.', + 222, + ], + [ + '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, + ], + ]); + } + + public function testBug2964(): void + { + $this->analyse([__DIR__ . '/data/bug2964.php'], []); + } + + 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) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-binary-nullsafe.php'], [ + [ + 'Binary operation "+" between array|null and \'2\' results in an error.', + 12, + ], + ]); + } + + 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 ef493ca348..3305d725c4 100644 --- a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php @@ -2,18 +2,21 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidComparisonOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidComparisonOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidComparisonOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), ); } @@ -140,7 +143,34 @@ 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, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-comparison-nullsafe.php'], [ + [ + 'Comparison operation "==" between stdClass|null and int results in an error.', + 12, + ], ]); } + 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 a5564251cd..63a9630c09 100644 --- a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php @@ -2,15 +2,26 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidIncDecOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidIncDecOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + 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 @@ -28,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 c978abb5e4..5b1b47c41e 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -2,15 +2,26 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidUnaryOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidUnaryOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + 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 @@ -18,19 +29,145 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/invalid-unary.php'], [ [ 'Unary operation "+" on string results in an error.', - 10, + 11, ], [ 'Unary operation "-" on string results in an error.', - 11, + 12, ], [ 'Unary operation "+" on \'bla\' results in an error.', - 16, + 19, ], [ 'Unary operation "-" on \'bla\' results in an error.', - 17, + 20, + ], + [ + '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-3515.php b/tests/PHPStan/Rules/Operators/data/bug-3515.php new file mode 100644 index 0000000000..277d601a4c --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-3515.php @@ -0,0 +1,13 @@ + 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 @@ +foo = 'bar'; + $a?->foo->bar = 'bar'; + $a?->foo->bar['foo'] = 'bar'; + + [$a?->foo->bar] = 'test'; + [$a?->foo->bar => $b, $f?->foo->bar => $c] = 'test'; + + $c = 'foo'; + } + + public function doBar( + \stdClass $s + ) + { + $s->foo = 'bar'; + $d = 'foo'; + $s['test'] = 'baz'; + \stdClass::$foo = 'bar'; + + $s->foo() = 'test'; + + [$s->foo()] = ['test']; + [$s] = ['test']; + } + + public function doBaz(?\stdClass $a, \stdClass $b) + { + $x = &$a?->bar->foo; + $y = &$a?->bar; + $z = $b?->bar; + } + + +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-binary-mixed.php new file mode 100644 index 0000000000..edd4eb52b0 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary-mixed.php @@ -0,0 +1,157 @@ + 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-nullsafe.php b/tests/PHPStan/Rules/Operators/data/invalid-binary-nullsafe.php new file mode 100644 index 0000000000..5227e1fcac --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary-nullsafe.php @@ -0,0 +1,13 @@ += 8.0 + +namespace InvalidBinaryNullsafe; + +class Bar +{ + public array $array; +} + +function dooFoo(?Bar $bar) +{ + $bar?->array + '2'; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index 9ea75a0589..60d71d4ba1 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -173,3 +173,100 @@ function (array $args) { function (array $args) { isset($args['y']) ? $args + [] : $args; }; + +/** + * @param non-empty-string $foo + * @param string $bar + * @param class-string $foobar + * @param literal-string $literalString + */ +function bug6624_should_error($foo, $bar, $foobar, $literalString) { + echo ($foo + 10); + echo ($foo - 10); + echo ($foo * 10); + echo ($foo / 10); + + echo (10 + $foo); + echo (10 - $foo); + echo (10 * $foo); + echo (10 / $foo); + + echo ($bar + 10); + echo ($bar - 10); + echo ($bar * 10); + echo ($bar / 10); + + echo (10 + $bar); + echo (10 - $bar); + echo (10 * $bar); + echo (10 / $bar); + + echo ($foobar + 10); + echo ($foobar - 10); + echo ($foobar * 10); + echo ($foobar / 10); + + echo (10 + $foobar); + echo (10 - $foobar); + echo (10 * $foobar); + echo (10 / $foobar); + + echo ($literalString + 10); + echo ($literalString - 10); + echo ($literalString * 10); + echo ($literalString / 10); + + echo (10 + $literalString); + echo (10 - $literalString); + echo (10 * $literalString); + echo (10 / $literalString); +} + +/** + * @param numeric-string $numericString + */ +function bug6624_no_error($numericString) { + echo ($numericString + 10); + echo ($numericString - 10); + echo ($numericString * 10); + echo ($numericString / 10); + + echo (10 + $numericString); + echo (10 - $numericString); + echo (10 * $numericString); + echo (10 / $numericString); + + $numericLiteral = "123"; + + echo ($numericLiteral + 10); + echo ($numericLiteral - 10); + echo ($numericLiteral * 10); + echo ($numericLiteral / 10); + + echo (10 + $numericLiteral); + echo (10 - $numericLiteral); + echo (10 * $numericLiteral); + echo (10 / $numericLiteral); +} + +function benevolentPlus(array $a, int $i): void { + foreach ($a as $k => $v) { + echo $k + $i; + } +}; + +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-nullsafe.php b/tests/PHPStan/Rules/Operators/data/invalid-comparison-nullsafe.php new file mode 100644 index 0000000000..e3c5338507 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-comparison-nullsafe.php @@ -0,0 +1,13 @@ += 8.0 + +namespace InvalidComparisonNullsafe; + +class Bar +{ + public \stdClass $val; +} + +function doFoo(?Bar $bar, int $a) +{ + $bar?->val == $a; +} diff --git a/tests/PHPStan/Rules/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 new file mode 100644 index 0000000000..a66be60a2d --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -0,0 +1,53 @@ + + */ +class IncompatibleClassConstantPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleClassConstantPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc.php'], [ + [ + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::FOO contains unresolvable type.', + 9, + ], + [ + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::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 IncompatibleClassConstantPhpDocNativeType\Foo::BAZ with type string is incompatible with native type int.', + 14, + ], + [ + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::LOREM with type int|string is not subtype of native type int.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/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 fd4f1459e0..b5ed921568 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -2,20 +2,47 @@ 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; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IncompatiblePhpDocTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class IncompatiblePhpDocTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new GenericObjectTypeCheck() + 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, + ), + ), + ), ); } @@ -69,14 +96,17 @@ public function testRule(): void [ 'PHPDoc tag @param for parameter $a with type T is not subtype of native type int.', 154, + 'Write @template T of int to fix this.', ], [ 'PHPDoc tag @param for parameter $b with type U of DateTimeInterface is not subtype of native type DateTime.', 154, + 'Write @template U of DateTime to fix this.', ], [ - 'PHPDoc tag @return with type DateTimeInterface is not subtype of native type DateTime.', + 'PHPDoc tag @return with type U of DateTimeInterface is not subtype of native type DateTime.', 154, + 'Write @template U of DateTime to fix this.', ], [ 'PHPDoc tag @param for parameter $foo contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', @@ -130,6 +160,329 @@ public function testRule(): void 'Type stdClass in generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc tag @param for parameter $x is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.', 242, ], + [ + 'Type stdClass in generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc tag @return is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.', + 250, + ], + [ + 'Generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc tag @return does not specify all template types of class InvalidPhpDocDefinitions\FooGeneric: T, U', + 266, + ], + [ + 'PHPDoc tag @return contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', + 274, + ], + [ + 'PHPDoc tag @param for parameter $i with type TFoo is not subtype of native type int.', + 283, + 'Write @template TFoo of int to fix this.', + ], + [ + '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, + ], + ]); + } + + public function testBug4643(): void + { + $this->analyse([__DIR__ . '/data/bug-4643.php'], []); + } + + public function testBug3753(): void + { + $this->analyse([__DIR__ . '/data/bug-3753.php'], [ + [ + 'PHPDoc tag @param for parameter $foo contains unresolvable type.', + 20, + ], + ]); + } + + public function testTemplateTypeNativeTypeObject(): void + { + $this->analyse([__DIR__ . '/data/template-type-native-type-object.php'], [ + [ + 'PHPDoc tag @return with type T is not subtype of native type object.', + 23, + 'Write @template T of object to fix this.', + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/generic-enum-param.php'], [ + [ + 'PHPDoc tag @param for parameter $e contains generic type GenericEnumParam\FooEnum but enum GenericEnumParam\FooEnum is not generic.', + 16, + ], + ]); + } + + 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 156ea878ab..b0ed51d449 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -2,18 +2,43 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IncompatiblePropertyPhpDocTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new IncompatiblePropertyPhpDocTypeRule(new GenericObjectTypeCheck()); + $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 @@ -55,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.', @@ -76,6 +107,93 @@ public function testNativeTypes(): void 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Foo::$stringOrInt with type int|string is not subtype of native type string.', 21, ], + [ + 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Lorem::$string with type T is not subtype of native type string.', + 45, + 'Write @template T of string to fix this.', + ], + ]); + } + + public function testPromotedProperties(): void + { + $this->analyse([__DIR__ . '/data/incompatible-property-promoted.php'], [ + [ + 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$bar contains unresolvable type.', + 16, + ], + [ + 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$classStringInt contains unresolvable type.', + 22, + ], + [ + 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$fooGeneric contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', + 28, + ], + [ + 'Generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$notEnoughTypesGenericfoo does not specify all template types of class InvalidPhpDocDefinitions\FooGeneric: T, U', + 34, + ], + [ + 'Generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$tooManyTypesGenericfoo specifies 3 template types, but class InvalidPhpDocDefinitions\FooGeneric supports only 2: T, U', + 37, + ], + [ + 'Type Throwable in generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$invalidTypeGenericfoo is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.', + 40, + ], + [ + 'Type stdClass in generic type InvalidPhpDocDefinitions\FooGeneric in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$anotherInvalidTypeGenericfoo is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.', + 43, + ], + [ + 'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$unknownClassConstant contains unresolvable type.', + 46, + ], + [ + '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 + { + $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 0f3ee70c93..e91e647054 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -4,18 +4,21 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidPHPStanDocTagRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidPHPStanDocTagRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidPHPStanDocTagRule( self::getContainer()->getByType(Lexer::class), - self::getContainer()->getByType(PhpDocParser::class) + self::getContainer()->getByType(PhpDocParser::class), ); } @@ -24,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 0d110c39fb..be63bff8e2 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -4,18 +4,21 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidPhpDocTagValueRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidPhpDocTagValueRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidPhpDocTagValueRule( self::getContainer()->getByType(Lexer::class), - self::getContainer()->getByType(PhpDocParser::class) + self::getContainer()->getByType(PhpDocParser::class), ); } @@ -23,64 +26,135 @@ 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 @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18 on line 1', + 80, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 88, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 91, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 101, + ], + ]); + } + + public function testBug4731(): void + { + $this->analyse([__DIR__ . '/data/bug-4731.php'], []); + } + + 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 89f4cf6ed5..ec26814c8f 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -3,29 +3,39 @@ 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; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ 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), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), new GenericObjectTypeCheck(), - new MissingTypehintCheck($broker, true, true), + new MissingTypehintCheck(true, []), + new UnresolvableTypeHelper(), + true, + true, true, - true ); } @@ -43,6 +53,7 @@ public function testRule(): void [ 'PHPDoc tag @var for variable $test contains unknown class InvalidVarTagType\aray.', 20, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'PHPDoc tag @var for variable $value contains unresolvable type.', @@ -83,12 +94,90 @@ public function testRule(): void [ 'PHPDoc tag @var for variable $test has no value type specified in iterable type array.', 58, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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%.', + ], + [ + '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', + ], + ]); + } + + public function testBug4486(): void + { + $this->analyse([__DIR__ . '/data/bug-4486.php'], [ + [ + 'PHPDoc tag @var for variable $one contains unknown class Some\Namespaced\ClassName1.', + 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @var for variable $two contains unknown class Some\Namespaced\ClassName2.', + 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @var for variable $three contains unknown class Some\Namespaced\ClassName1.', + 15, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug4486Namespace(): void + { + $this->analyse([__DIR__ . '/data/bug-4486-ns.php'], [ + [ + 'PHPDoc tag @var for variable $one contains unknown class Bug4486Namespace\ClassName1.', + 6, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @var for variable $two contains unknown class Bug4486Namespace\ClassName1.', + 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug6252(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-6252.php'], []); + } + + public function testBug6348(): void + { + $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 9b28137f50..e378f873b6 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -2,16 +2,22 @@ namespace PHPStan\Rules\PhpDoc; +use InvalidThrowsPhpDocMergeInherited\Four; +use InvalidThrowsPhpDocMergeInherited\Three; +use InvalidThrowsPhpDocMergeInherited\Two; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidThrowsPhpDocValueRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidThrowsPhpDocValueRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidThrowsPhpDocValueRule(self::getContainer()->getByType(FileTypeMapper::class)); } @@ -47,6 +53,10 @@ public function testRule(): void 'PHPDoc tag @throws with type stdClass|void is not subtype of Throwable', 103, ], + [ + 'PHPDoc tag @throws with type stdClass is not subtype of Throwable', + 118, + ], ]); } @@ -68,21 +78,43 @@ 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 [ [ - \InvalidThrowsPhpDocMergeInherited\Two::class, + Two::class, 'method', 'InvalidThrowsPhpDocMergeInherited\C|InvalidThrowsPhpDocMergeInherited\D', ], [ - \InvalidThrowsPhpDocMergeInherited\Three::class, + Three::class, 'method', 'InvalidThrowsPhpDocMergeInherited\C|InvalidThrowsPhpDocMergeInherited\D', ], [ - \InvalidThrowsPhpDocMergeInherited\Four::class, + Four::class, 'method', 'InvalidThrowsPhpDocMergeInherited\C|InvalidThrowsPhpDocMergeInherited\D', ], @@ -91,17 +123,14 @@ public function dataMergeInheritedPhpDocs(): array /** * @dataProvider dataMergeInheritedPhpDocs - * @param string $className - * @param string $method - * @param string $expectedType */ public function testMergeInheritedPhpDocs( string $className, string $method, - string $expectedType + string $expectedType, ): void { - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $method = $reflection->getNativeMethod($method); $throwsType = $method->getThrowType(); @@ -109,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..68e2bfd718 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php @@ -0,0 +1,99 @@ + + */ +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 an interface IncompatibleRequireExtends\SomeInterface, expected a class.', + 13, + 'If you meant an interface, use @phpstan-require-implements instead.', + ], + [ + '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 cannot contain an interface IncompatibleRequireExtends\UnresolvableExtendsInterface, expected a class.', + 135, + 'If you meant an interface, use @phpstan-require-implements instead.', + ], + [ + 'PHPDoc tag @phpstan-require-extends can only be used once.', + 178, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\NonExistentClass.', + 183, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\SomeClass.', + 183, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php new file mode 100644 index 0000000000..96f423723c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php @@ -0,0 +1,69 @@ + + */ +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, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\NonExistentClass.', + 192, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\SomeClass.', + 192, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + +} 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 f977233b3d..ea87452e90 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -2,26 +2,47 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class WrongVariableNameInVarTagRuleTest extends RuleTestCase { + private bool $checkTypeAgainstPhpDocType = false; + + private bool $strictWideningCheck = false; + protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( - self::getContainer()->getByType(FileTypeMapper::class) + 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, @@ -31,11 +52,7 @@ public function testRule(): void 23, ], [ - 'Variable $list in PHPDoc tag @var does not match any variable in the foreach loop: $key, $var', - 29, - ], - [ - 'Variable $foo in PHPDoc tag @var does not match any variable in the foreach loop: $key, $val', + 'Variable $foo in PHPDoc tag @var does not match any variable in the foreach loop: $list, $key, $val', 66, ], [ @@ -66,21 +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.', + 211, + ], + [ + 'PHPDoc tag @var above foreach loop does not specify variable name.', + 235, + ], + [ + 'Variable $foo in PHPDoc tag @var does not exist.', + 249, + ], + [ + 'Variable $bar in PHPDoc tag @var does not exist.', + 249, + ], + [ + 'Variable $slots in PHPDoc tag @var does not exist.', + 263, + ], + [ + 'Variable $slots in PHPDoc tag @var does not exist.', + 269, + ], + [ + 'PHPDoc tag @var above assignment does not specify variable name.', + 275, + ], + [ + 'Variable $slots in PHPDoc tag @var does not match assigned variable $itemSlots.', + 281, + ], + [ + 'PHPDoc tag @var above a class has no effect.', + 301, + ], + [ + 'PHPDoc tag @var above a method has no effect.', + 305, + ], + [ + 'PHPDoc tag @var above a function has no effect.', + 313, ], ]); } @@ -90,4 +155,423 @@ public function testEmptyFileWithVarThis(): void $this->analyse([__DIR__ . '/data/wrong-variable-name-var-empty-this.php'], []); } + public function testAboveUse(): void + { + $this->analyse([__DIR__ . '/data/var-above-use.php'], []); + } + + public function testAboveDeclare(): void + { + $this->analyse([__DIR__ . '/data/var-above-declare.php'], []); + } + + public function testBug3515(): void + { + $this->analyse([__DIR__ . '/data/bug-3515.php'], []); + } + + public function testBug4500(): void + { + $this->analyse([__DIR__ . '/data/bug-4500.php'], [ + [ + 'PHPDoc tag @var above multiple global variables does not specify variable name.', + 23, + ], + [ + 'Variable $baz in PHPDoc tag @var does not match any global variable: $lorem', + 43, + ], + [ + 'Variable $baz in PHPDoc tag @var does not match any global variable: $lorem', + 49, + ], + ]); + } + + public function testBug4504(): void + { + $this->analyse([__DIR__ . '/data/bug-4504.php'], []); + } + + 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) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/wrong-var-enum.php'], [ + [ + 'PHPDoc tag @var above an enum has no effect.', + 13, + ], + ]); + } + + 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-3515.php b/tests/PHPStan/Rules/PhpDoc/data/bug-3515.php new file mode 100644 index 0000000000..585d9d6f18 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-3515.php @@ -0,0 +1,4 @@ + $foo + */ + public function doFoo(array $foo): void + { + + } + + /** + * @param array $bars + */ + public function doBar(array $bars): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php new file mode 100644 index 0000000000..41bc50eae0 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4227.php @@ -0,0 +1,20 @@ +property = $property; + } + + public function count(): int + { + return $this->count; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4486-ns.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4486-ns.php new file mode 100644 index 0000000000..e914313cb2 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4486-ns.php @@ -0,0 +1,10 @@ + $models */ + foreach ($models as $k => $v) { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4505.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4505.php new file mode 100644 index 0000000000..3093732b2d --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4505.php @@ -0,0 +1,17 @@ + */ +class UserRepository extends Repository +{ + + function store(Entity $entity): Entity + { + assertType('F of Bug4643\User (method Bug4643\Repository::store(), argument)', $entity); + return $entity; + } + +} + +function (UserRepository $r): void { + assertType(User::class, $r->store(new User())); + assertType(Admin::class, $r->store(new Admin())); + assertType('F of Bug4643\User (method Bug4643\Repository::store(), parameter)', $r->store(new Article())); // should be User::class, but inheriting template tags is now broken like that +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4731-no-first-tag.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4731-no-first-tag.php new file mode 100644 index 0000000000..692897caee --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4731-no-first-tag.php @@ -0,0 +1,44 @@ +, + * type:int, + * subtype:string + * } + * @psalm-type HOSTNAMEANDADDRESS_ENTRY = object{host?:string, personal?:string, mailbox:string} + * @psalm-type HOSTNAMEANDADDRESS = array{0:HOSTNAMEANDADDRESS_ENTRY, 1?:HOSTNAMEANDADDRESS_ENTRY} + * @psalm-type COMPOSE_ENVELOPE = array{ + * subject?:string + * } + * @psalm-type COMPOSE_BODY = list + * + * @todo see @todo of Imap::mail_compose() + */ +class HelloWorld +{ + public function sayHello(\DateTimeImmutable $date): void + { + echo 'Hello, ' . $date->format('j. n. Y'); + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-4731.php b/tests/PHPStan/Rules/PhpDoc/data/bug-4731.php new file mode 100644 index 0000000000..5f58cb740e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-4731.php @@ -0,0 +1,44 @@ +, + * type:int, + * subtype:string + * } + * @psalm-type HOSTNAMEANDADDRESS_ENTRY = object{host?:string, personal?:string, mailbox:string} + * @psalm-type HOSTNAMEANDADDRESS = array{0:HOSTNAMEANDADDRESS_ENTRY, 1?:HOSTNAMEANDADDRESS_ENTRY} + * @psalm-type COMPOSE_ENVELOPE = array{ + * subject?:string + * } + * @psalm-type COMPOSE_BODY = list + * + * @todo see @todo of Imap::mail_compose() + */ +class HelloWorld +{ + public function sayHello(\DateTimeImmutable $date): void + { + echo 'Hello, ' . $date->format('j. n. Y'); + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-6252.php b/tests/PHPStan/Rules/PhpDoc/data/bug-6252.php new file mode 100644 index 0000000000..9d9416fc77 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-6252.php @@ -0,0 +1,15 @@ +#!/usr/bin/env php + [], 'branches' => ['names' => [], 'exclude' => false]]; + } + else { + return 0; + } + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-6348.php b/tests/PHPStan/Rules/PhpDoc/data/bug-6348.php new file mode 100644 index 0000000000..294478e1ae --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-6348.php @@ -0,0 +1,14 @@ += 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/generic-enum-param.php b/tests/PHPStan/Rules/PhpDoc/data/generic-enum-param.php new file mode 100644 index 0000000000..c435b03503 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-enum-param.php @@ -0,0 +1,21 @@ += 8.1 + +namespace GenericEnumParam; + +enum FooEnum +{ + +} + +class Foo +{ + + /** + * @param FooEnum $e + */ + public function doFoo(FooEnum $e): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/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 new file mode 100644 index 0000000000..d2b9714425 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php @@ -0,0 +1,14 @@ + */ + const DOLOR = 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 93a4a38a91..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 new file mode 100644 index 0000000000..ebf011438b --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-promoted.php @@ -0,0 +1,66 @@ += 8.0 + +namespace InvalidPhpDocPromotedProperties; + +use InvalidPhpDoc\Foo; +use InvalidPhpDoc\Bar; + +class FooWithProperty +{ + + public function __construct( + /** @var aray */ + private $foo, + + /** @var Foo&Bar */ + private $bar, + + /** @var never */ + private $baz, + + /** @var class-string */ + private $classStringInt, + + /** @var class-string */ + private $classStringValid, + + /** @var array{\InvalidPhpDocDefinitions\Foo<\stdClass>} */ + private $fooGeneric, + + /** @var \InvalidPhpDocDefinitions\FooGeneric */ + private $validGenericFoo, + + /** @var \InvalidPhpDocDefinitions\FooGeneric */ + private $notEnoughTypesGenericfoo, + + /** @var \InvalidPhpDocDefinitions\FooGeneric */ + private $tooManyTypesGenericfoo, + + /** @var \InvalidPhpDocDefinitions\FooGeneric */ + private $invalidTypeGenericfoo, + + /** @var \InvalidPhpDocDefinitions\FooGeneric */ + private $anotherInvalidTypeGenericfoo, + + /** @var UnknownClass::BLABLA */ + private $unknownClassConstant, + + /** @var self::BLABLA */ + private $unknownClassConstant2 + ) { } + +} + +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..6b5af9e340 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php @@ -0,0 +1,200 @@ += 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 {} + +/** + * @phpstan-require-extends SomeClass|NonExistentClass + */ +interface RequireNonExisstentUnionClassinterface {} + +new class implements RequireNonExisstentUnionClassinterface {}; + +new class extends SomeClass implements RequireNonExisstentUnionClassinterface {}; + +/** + * @phpstan-require-extends SomeClass|NonExistentClass + */ +trait RequireNonExisstentUnionClassTrait {} + +new class { + use RequireNonExisstentUnionClassTrait; +}; + +new class extends SomeClass { + use RequireNonExisstentUnionClassTrait; +}; 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-throws.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-throws.php index edf27b326a..7ce3088fc3 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-throws.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-throws.php @@ -111,3 +111,9 @@ function voidUnionWithNotThrowableThrows() function exceptionTemplateThrows() { } + +function inlineThrows() +{ + /** @throws \stdClass */ + $i = 1; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index feca1c06f2..078e004d26 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -243,3 +243,89 @@ function genericGenerics($t, $u, $v, $w, $x) { } + +/** + * @return \InvalidPhpDocDefinitions\FooGeneric<\InvalidPhpDocDefinitions\FooGeneric, \Exception> + */ +function genericNestedWrongTemplateArgs() +{ + +} + +/** + * @return \InvalidPhpDocDefinitions\FooGeneric<\InvalidPhpDocDefinitions\FooGeneric, \Exception> + */ +function genericNestedOkTemplateArgs() +{ + +} + +/** + * @return \InvalidPhpDocDefinitions\FooGeneric<\InvalidPhpDocDefinitions\FooGeneric, \Exception> + */ +function genericNestedWrongArgCount() +{ + +} + +/** + * @return \InvalidPhpDocDefinitions\FooGeneric<\InvalidPhpDocDefinitions\Foo, \Exception> + */ +function genericNestedNonTemplateArgs() +{ + +} + +/** + * @template TFoo + * @param TFoo $i + */ +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 353078efd4..8b0cf8bd81 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php @@ -62,3 +62,44 @@ class Baz private $barProperty; } + +class InlineThrows +{ + + public function doFoo() + { + /** @throws (\Exception */ + $i = 1; + } + +} + +class ClassConstant +{ + + /** @var (Foo|Bar */ + 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 bede104fa6..60a35c8178 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php @@ -27,4 +27,34 @@ function baz() /** @phpstan-va */$a = $b; /** @phpstan-ignore-line */ $c = 'foo'; } + + /** + * @phpstan-throws void + */ + 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) + { + /** @var Blabla $foo */ + if (true) { + + } } } @@ -67,3 +93,19 @@ trait FooTrait { } + +class Bar +{ + + /** @var Blabla */ + private $foo; + +} + +class Baz +{ + + /** @var self&\stdClass */ + const FOO = 1; + +} 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 new file mode 100644 index 0000000000..215b330ff9 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php @@ -0,0 +1,31 @@ + + */ + private array $instances; + + public function __construct() + { + $this->instances = []; + } + + /** + * @phpstan-template T + * @phpstan-param class-string $className + * + * @phpstan-return T + */ + public function getInstanceByName(string $className, string $name): object + { + $instance = $this->instances["[{$className}]{$name}"]; + + \assert($instance instanceof $className); + + return $instance; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php b/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php new file mode 100644 index 0000000000..9e816d8a66 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php @@ -0,0 +1,76 @@ += 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-declare.php b/tests/PHPStan/Rules/PhpDoc/data/var-above-declare.php new file mode 100644 index 0000000000..0f0e1de345 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-above-declare.php @@ -0,0 +1,7 @@ + $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-above-use.php b/tests/PHPStan/Rules/PhpDoc/data/var-above-use.php new file mode 100644 index 0000000000..9200e669cf --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-above-use.php @@ -0,0 +1,5 @@ +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-enum.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-enum.php new file mode 100644 index 0000000000..6153fc4db6 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-enum.php @@ -0,0 +1,16 @@ += 8.1 + +namespace WrongVarEnum; + +enum Foo +{ + +} + +/** + * @var Foo $test + */ +enum Bar +{ + +} diff --git a/tests/PHPStan/Rules/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 3db6581639..30f7dd3182 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php @@ -26,7 +26,7 @@ public function doFoo() public function doBar(array $list) { /** @var int[] $list */ - foreach ($list as $key => $var) { // ERROR + foreach ($list as $key => $var) { } @@ -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 @@ -188,4 +189,145 @@ public function thisInVar2() $demo = $this->demo(); } + public function overrideDifferentVariableAboveAssign() + { + $foo = 'foo'; + + /** @var int $foo */ + $bar = $foo + 1; + } + + public function testIf($foo) + { + /** @var int $foo */ + do { + + } while (true); + } + + public function testIf2() + { + /** @var int $foo */ + do { + + } while (true); + } + +} + +class Bar +{ + + /** @var int */ + private $test; + + /** @var string */ + const TEST = 'str'; + +} + +class ForeachJustValueVar +{ + + public function doBar(array $list) + { + /** @var int */ + foreach ($list as $val) { + + } + } + +} + +class MultipleDocComments +{ + + public function doFoo(): void + { + /** @var int $foo */ + /** @var string $bar */ + echo 'foo'; + } + + public function doBar(array $slots): void + { + /** @var \stdClass[] $itemSlots */ + /** @var \stdClass[] $slots */ + $itemSlots = []; + } + + public function doBaz(): void + { + /** @var \stdClass[] $itemSlots */ + /** @var \stdClass[] $slots */ + $itemSlots = []; + } + + public function doLorem(): void + { + /** @var \stdClass[] $slots */ + $itemSlots['foo'] = 'bar'; + } + + public function doIpsum(): void + { + /** @var \stdClass[] */ + $itemSlots['foo'] = 'bar'; + } + + public function doDolor(): void + { + /** @var \stdClass[] $slots */ + $itemSlots = []; + + /** @var int $test */ + [[$test]] = doFoo(); + } + + public function doSit(): void + { + /** + * @var int $foo + * @var int $bar + */ + [$foo, $bar] = doFoo(); + } + +} + +/** + * @var string + */ +class VarInWrongPlaces +{ + + /** @var int $a */ + public function doFoo($a) + { + + } + +} + +/** @var int */ +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 @@ + */ +class AccessPrivatePropertyThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AccessPrivatePropertyThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/access-private-property-static.php'], [ + [ + 'Unsafe access to private property AccessPrivatePropertyThroughStatic\Foo::$foo through static::.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index a83bf3c7c3..0640efd885 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -2,30 +2,164 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class AccessPropertiesInAssignRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($broker, new RuleLevelHelper($broker, true, false, true, false), true) + new 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 + { + $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 14eaf47416..3fab4606d8 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -2,170 +2,180 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AccessPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class AccessPropertiesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkDynamicProperties; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new AccessPropertiesRule($broker, new RuleLevelHelper($broker, true, $this->checkThisOnly, $this->checkUnionTypes, false), true); + $reflectionProvider = $this->createReflectionProvider(); + return new AccessPropertiesRule(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, + 274, ], [ - 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', - 299, + 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', + 302, + $tipText, ], [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, - ], - [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, + 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', + 407, ], [ - 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', - 402, + 'Access to an undefined property object::$baz.', + 438, + $tipText, ], - ] + ], ); } @@ -173,160 +183,185 @@ 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, + 274, ], [ - 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', - 299, + 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', + 302, + $tipText, ], - [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, - ], - [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, - ], - ] + ], ); } + public function testRuleAssignOp(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ + [ + 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', + 10, + $tipText, + ], + ]); + } + 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\AccessPropertyWithDimFetch::$foo.', - 364, - ], - [ - '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.', @@ -339,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.', @@ -355,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, ], ]); } @@ -367,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, ], ]); } @@ -379,23 +422,28 @@ public function testClassExists(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/access-properties-class-exists.php'], [ [ 'Access to property $lorem on an unknown class AccessPropertiesClassExists\Bar.', 15, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to property $lorem on an unknown class AccessPropertiesClassExists\Baz.', 15, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to property $lorem on an unknown class AccessPropertiesClassExists\Baz.', 18, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to property $lorem on an unknown class AccessPropertiesClassExists\Bar.', 22, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); } @@ -404,12 +452,595 @@ 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, ], ]); } + public function testBug3947(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-3947.php'], []); + } + + public function testNullSafe(): 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/nullsafe-property-fetch.php'], [ + [ + 'Access to an undefined property NullsafePropertyFetch\Foo::$baz.', + 13, + $tipText, + ], + [ + 'Cannot access property $bar on string.', + 18, + ], + [ + 'Cannot access property $bar on string.', + 19, + ], + [ + 'Cannot access property $bar on string.', + 21, + ], + [ + 'Cannot access property $bar on string.', + 22, + ], + [ + 'Cannot access property $foo on null.', + 28, + ], + [ + 'Cannot access property $foo on null.', + 29, + ], + ]); + } + + public function testBug3371(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-3371.php'], []); + } + + public function testBug4527(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-4527.php'], []); + } + + 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) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-5868.php'], [ + [ + 'Cannot access property $child on Bug5868PropertyFetch\Foo|null.', + 31, + ], + [ + 'Cannot access property $child on Bug5868PropertyFetch\Child|null.', + 32, + ], + [ + 'Cannot access property $existingChild on Bug5868PropertyFetch\Child|null.', + 33, + ], + [ + 'Cannot access property $existingChild on Bug5868PropertyFetch\Child|null.', + 34, + ], + ]); + } + + public function testBug6385(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->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, + ], + ]); + } + + public function testBug6566(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $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 b115855c9d..355cf0dfa2 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -3,21 +3,33 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class AccessStaticPropertiesInAssignRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesInAssignRule( - new AccessStaticPropertiesRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker)) + new AccessStaticPropertiesRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + true, + ), ); } @@ -26,9 +38,33 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/access-static-properties-assign.php'], [ [ 'Access to an undefined static property TestAccessStaticPropertiesAssign\AccessStaticPropertyWithDimFetch::$foo.', + 10, + ], + [ + 'Access to an undefined static property TestAccessStaticPropertiesAssign\AccessStaticPropertyWithDimFetch::$foo.', + 15, + ], + ]); + } + + public function testRuleAssignOp(): void + { + $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ + [ + 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', 15, ], ]); } + public function testRuleExpressionNames(): void + { + $this->analyse([__DIR__ . '/data/properties-from-array-into-static-object.php'], [ + [ + 'Access to an undefined static property PropertiesFromArrayIntoStaticObject\Foo::$noop.', + 29, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index b4ac8d1c7f..bae249fd95 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -3,29 +3,36 @@ 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; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AccessStaticPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class AccessStaticPropertiesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesRule( - $broker, - new RuleLevelHelper($broker, true, false, true, false), - new ClassCaseSensitivityCheck($broker) + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, 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.', @@ -43,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, @@ -54,37 +65,90 @@ public function testAccessStaticProperties(): void [ 'Access to static property $test on an unknown class UnknownStaticProperties.', 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 IpsumAccessStaticProperties::$baz.', + '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 IpsumAccessStaticProperties::$nonexistent.', + '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 IpsumAccessStaticProperties::$emptyBaz.', + '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 IpsumAccessStaticProperties::$emptyNonexistent.', + '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 IpsumAccessStaticProperties::$anotherNonexistent.', + '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 IpsumAccessStaticProperties::$anotherNonexistent.', + '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 IpsumAccessStaticProperties::$anotherEmptyNonexistent.', + '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 IpsumAccessStaticProperties::$anotherEmptyNonexistent.', + '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, ], [ @@ -114,6 +178,7 @@ public function testAccessStaticProperties(): void [ 'Access to static property $test on an unknown class NonexistentClass.', 97, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Access to an undefined static property FooAccessStaticProperties&SomeInterface::$nonexistent.', @@ -155,25 +220,60 @@ 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 AccessPropertyWithDimFetch::$foo.', - 163, + 'Access to an undefined static property AccessInIsset::$foo.', + 178, ], [ 'Access to an undefined static property AccessInIsset::$foo.', 185, ], - [ - 'Access to static property $foo on trait TraitWithStaticProperty.', - 204, - ], [ 'Access to static property $foo on an unknown class TraitWithStaticProperty.', 209, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Static access to instance property AccessWithStatic::$bar.', + 223, + ], + [ + 'Access to an undefined static property static(AccessWithStatic)::$nonexistent.', + 224, + ], + [ + '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 + { + $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ + [ + 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', + 10, ], ]); } @@ -183,4 +283,33 @@ public function testClassExists(): void $this->analyse([__DIR__ . '/data/static-properties-class-exists.php'], []); } + 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 9588d3f87d..ad1a9b43a9 100644 --- a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php @@ -4,16 +4,17 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DefaultValueTypesAssignedToPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class DefaultValueTypesAssignedToPropertiesRuleTest extends RuleTestCase { protected function getRule(): Rule { - 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 @@ -36,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.', @@ -47,4 +45,24 @@ public function testDefaultValueForNativePropertyType(): void ]); } + public function testBug5607(): void + { + $this->analyse([__DIR__ . '/data/bug-5607.php'], [ + [ + 'Property Bug5607\Cl::$u (Bug5607\A|null) does not accept default value of type array.', + 10, + ], + ]); + } + + 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 afc0245723..873654f585 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -2,23 +2,39 @@ namespace PHPStan\Rules\Properties; +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; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInPropertiesRuleTest extends RuleTestCase { + private int $phpVersion = PHP_VERSION_ID; + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInPropertiesRule( - $broker, - new ClassCaseSensitivityCheck($broker), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + $reflectionProvider, + self::getContainer(), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersion), + true, + false, true, - false ); } @@ -32,26 +48,32 @@ public function testNonexistentClass(): void [ 'Property PropertiesTypes\Foo::$bar has unknown class PropertiesTypes\Bar as its type.', 12, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Property PropertiesTypes\Foo::$bars has unknown class PropertiesTypes\Bar as its type.', 18, + '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', ], [ 'Class PropertiesTypes\Foo referenced with incorrect case: PropertiesTypes\FOO.', @@ -61,38 +83,98 @@ public function testNonexistentClass(): void 'Property PropertiesTypes\Foo::$withTrait has invalid type PropertiesTypes\SomeTrait.', 27, ], + [ + 'Class DateTime referenced with incorrect case: Datetime.', + 30, + ], [ 'Property PropertiesTypes\Foo::$nonexistentClassInGenericObjectType has unknown class PropertiesTypes\Foooo as its type.', 33, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Property PropertiesTypes\Foo::$nonexistentClassInGenericObjectType has unknown class PropertiesTypes\Barrrr as its type.', 33, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], - ] + ], ); } 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.', 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Property PropertiesNativeTypes\Foo::$baz has unknown class PropertiesNativeTypes\Baz as its type.', 13, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Property PropertiesNativeTypes\Foo::$baz has unknown class PropertiesNativeTypes\Baz as its type.', 13, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testPromotedProperties(): void + { + $this->analyse([__DIR__ . '/data/properties-promoted-types.php'], [ + [ + 'Property PromotedPropertiesExistingClasses\Foo::$baz has invalid type PromotedPropertiesExistingClasses\SomeTrait.', + 11, + ], + [ + 'Property PromotedPropertiesExistingClasses\Foo::$lorem has invalid type PromotedPropertiesExistingClasses\SomeTrait.', + 12, + ], + [ + 'Property PromotedPropertiesExistingClasses\Foo::$ipsum has unknown class PromotedPropertiesExistingClasses\Bar as its type.', + 13, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Property PromotedPropertiesExistingClasses\Foo::$dolor has unknown class PromotedPropertiesExistingClasses\Bar as its type.', + 14, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); } + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Property PropertyIntersectionTypes\Test::$prop2 has unresolvable native type.', + 30, + ], + [ + 'Property PropertyIntersectionTypes\Test::$prop3 has unresolvable native type.', + 32, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param list $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + $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 2f62ed6a73..e17100bc38 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -3,48 +3,60 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingPropertyTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingPropertyTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingPropertyTypehintRule(new MissingTypehintCheck($broker, true, true)); + return new MissingPropertyTypehintRule(new MissingTypehintCheck(true, [])); } public function testRule(): void { $this->analyse([__DIR__ . '/data/missing-property-typehint.php'], [ [ - 'Property MissingPropertyTypehint\MyClass::$prop1 has no typehint specified.', + 'Property MissingPropertyTypehint\MyClass::$prop1 has no type specified.', 7, ], [ - 'Property MissingPropertyTypehint\MyClass::$prop2 has no typehint specified.', + 'Property MissingPropertyTypehint\MyClass::$prop2 has no type specified.', 9, ], [ - 'Property MissingPropertyTypehint\MyClass::$prop3 has no typehint specified.', + 'Property MissingPropertyTypehint\MyClass::$prop3 has no type specified.', 14, ], [ 'Property MissingPropertyTypehint\ChildClass::$unionProp type has no value type specified in iterable type array.', 32, - "Consider adding something like array to the PHPDoc.\nYou can turn off this check by setting checkMissingIterableValueType: false in your %configurationFile%.", + 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.', + 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, ], ]); } @@ -54,4 +66,9 @@ public function testBug3402(): void $this->analyse([__DIR__ . '/data/bug-3402.php'], []); } + public function testPromotedProperties(): void + { + $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 new file mode 100644 index 0000000000..f389c3f628 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -0,0 +1,414 @@ + + */ +class MissingReadOnlyPropertyAssignRuleTest extends RuleTestCase +{ + + private bool $shouldNarrowMethodScopeFromConstructor = false; + + protected function getRule(): Rule + { + 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 + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-readonly-property-assign.php'], [ + [ + 'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned. Assign it in the constructor.', + 14, + ], + [ + 'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned2. Assign it in the constructor.', + 16, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\Foo::$readBeforeAssigned.', + 33, + ], + [ + 'Readonly property MissingReadOnlyPropertyAssign\Foo::$doubleAssigned is already assigned.', + 37, + ], + [ + 'Class MissingReadOnlyPropertyAssign\BarDoubleAssignInSetter has an uninitialized readonly property $foo. Assign it in the constructor.', + 53, + ], + [ + '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, + ], + [ + '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 new file mode 100644 index 0000000000..07300834bf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -0,0 +1,85 @@ + + */ +class NullsafePropertyFetchRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NullsafePropertyFetchRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-rule.php'], [ + [ + 'Using nullsafe property access on non-nullable type Exception. Use -> instead.', + 16, + ], + ]); + } + + public function testBug6020(): void + { + $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-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 new file mode 100644 index 0000000000..c5f5cd929c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -0,0 +1,283 @@ + + */ +class OverridingPropertyRuleTest extends RuleTestCase +{ + + private bool $reportMaybes; + + protected function getRule(): Rule + { + return new OverridingPropertyRule( + self::getContainer()->getByType(PhpVersion::class), + true, + $this->reportMaybes, + ); + } + + public function testRule(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/overriding-property.php'], [ + [ + 'Static property OverridingProperty\Bar::$protectedFoo overrides non-static property OverridingProperty\Foo::$protectedFoo.', + 25, + ], + [ + 'Non-static property OverridingProperty\Bar::$protectedStaticFoo overrides static property OverridingProperty\Foo::$protectedStaticFoo.', + 26, + ], + [ + 'Static property OverridingProperty\Bar::$publicFoo overrides non-static property OverridingProperty\Foo::$publicFoo.', + 28, + ], + [ + 'Non-static property OverridingProperty\Bar::$publicStaticFoo overrides static property OverridingProperty\Foo::$publicStaticFoo.', + 29, + ], + [ + 'Readonly property OverridingProperty\ReadonlyChild::$readWrite overrides readwrite property OverridingProperty\ReadonlyParent::$readWrite.', + 45, + ], + [ + 'Readwrite property OverridingProperty\ReadonlyChild::$readOnly overrides readonly property OverridingProperty\ReadonlyParent::$readOnly.', + 46, + ], + [ + 'Readonly property OverridingProperty\ReadonlyChild2::$readWrite overrides readwrite property OverridingProperty\ReadonlyParent::$readWrite.', + 55, + ], + [ + 'Readwrite property OverridingProperty\ReadonlyChild2::$readOnly overrides readonly property OverridingProperty\ReadonlyParent::$readOnly.', + 56, + ], + [ + 'Private property OverridingProperty\PrivateDolor::$protectedFoo overriding protected property OverridingProperty\Dolor::$protectedFoo should be protected or public.', + 76, + ], + [ + 'Private property OverridingProperty\PrivateDolor::$publicFoo overriding public property OverridingProperty\Dolor::$publicFoo should also be public.', + 77, + ], + [ + 'Private property OverridingProperty\PrivateDolor::$anotherPublicFoo overriding public property OverridingProperty\Dolor::$anotherPublicFoo should also be public.', + 78, + ], + [ + 'Protected property OverridingProperty\ProtectedDolor::$publicFoo overriding public property OverridingProperty\Dolor::$publicFoo should also be public.', + 87, + ], + [ + 'Protected property OverridingProperty\ProtectedDolor::$anotherPublicFoo overriding public property OverridingProperty\Dolor::$anotherPublicFoo should also be public.', + 88, + ], + [ + 'Property OverridingProperty\TypeChild::$withType overriding property OverridingProperty\Typed::$withType (int) should also have native type int.', + 125, + ], + [ + 'Property OverridingProperty\TypeChild::$withoutType (int) overriding property OverridingProperty\Typed::$withoutType should not have a native type.', + 126, + ], + [ + 'Type string of property OverridingProperty\Typed2Child::$foo is not the same as type int of overridden property OverridingProperty\Typed2::$foo.', + 142, + ], + [ + 'PHPDoc type 4 of property OverridingProperty\Typed2WithPhpDoc::$foo is not the same as PHPDoc type 1|2|3 of overridden property OverridingProperty\TypedWithPhpDoc::$foo.', + 158, + sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ), + ], + ]); + } + + public function dataRulePHPDocTypes(): array + { + $tip = sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ); + $tipWithOption = sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s\n This error can be turned off by setting\n %s", + 'https://phpstan.org/user-guide/stub-files', + 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', + ); + + return [ + [ + false, + [ + [ + 'PHPDoc type array of property OverridingPropertyPhpDoc\Bar::$arrayClassStrings is not covariant with PHPDoc type array of overridden property OverridingPropertyPhpDoc\Foo::$arrayClassStrings.', + 26, + $tip, + ], + [ + 'PHPDoc type int of property OverridingPropertyPhpDoc\Bar::$string is not covariant with PHPDoc type string of overridden property OverridingPropertyPhpDoc\Foo::$string.', + 29, + $tip, + ], + ], + ], + [ + true, + [ + [ + 'PHPDoc type array of property OverridingPropertyPhpDoc\Bar::$array is not the same as PHPDoc type array of overridden property OverridingPropertyPhpDoc\Foo::$array.', + 23, + $tipWithOption, + ], + [ + 'PHPDoc type array of property OverridingPropertyPhpDoc\Bar::$arrayClassStrings is not the same as PHPDoc type array of overridden property OverridingPropertyPhpDoc\Foo::$arrayClassStrings.', + 26, + $tip, + ], + [ + 'PHPDoc type int of property OverridingPropertyPhpDoc\Bar::$string is not the same as PHPDoc type string of overridden property OverridingPropertyPhpDoc\Foo::$string.', + 29, + $tip, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRulePHPDocTypes + * @param list $errors + */ + public function testRulePHPDocTypes(bool $reportMaybes, array $errors): void + { + $this->reportMaybes = $reportMaybes; + $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 new file mode 100644 index 0000000000..00874ab151 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -0,0 +1,73 @@ + + */ +class PropertyAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new PropertyAttributesRule( + 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 + { + $this->analyse([__DIR__ . '/data/property-attributes.php'], [ + [ + 'Attribute class PropertyAttributes\Foo does not have the property target.', + 26, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..e8acef73f8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php @@ -0,0 +1,48 @@ + + */ +class ReadOnlyPropertyAssignRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyPropertyAssignRefRule(new PropertyReflectionFinder()); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = [ + [ + 'Readonly property ReadOnlyPropertyAssignRef\Foo::$foo is assigned by reference.', + 14, + ], + [ + '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 new file mode 100644 index 0000000000..d54ae3a02f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -0,0 +1,194 @@ + + */ +class ReadOnlyPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyPropertyAssignRule( + new PropertyReflectionFinder(), + new ConstructorsHelper( + self::getContainer(), + [ + 'ReadonlyPropertyAssign\\TestCase::setUp', + ], + ), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1'); + } + + $errors = [ + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$foo is assigned outside of the constructor.', + 21, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$bar is assigned outside of its declaring class.', + 33, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', + 34, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$bar is assigned outside of its declaring class.', + 39, + ], + ]; + + 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, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooArrays::$details is assigned outside of the constructor.', + 65, + ], + [ + 'Readonly property ReadonlyPropertyAssign\NotThis::$foo is not assigned on $this.', + 90, + ], + [ + 'Readonly property ReadonlyPropertyAssign\PostInc::$foo is assigned outside of the constructor.', + 102, + ], + [ + 'Readonly property ReadonlyPropertyAssign\PostInc::$foo is assigned outside of the constructor.', + 103, + ], + [ + 'Readonly property ReadonlyPropertyAssign\PostInc::$foo is assigned outside of the constructor.', + 105, + ], + [ + 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', + 122, + ], + [ + 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', + 127, + ], + /*[ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of the constructor.', + 140, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of the constructor.', + 141, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of its declaring class.', + 151, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of its declaring class.', + 152, + ],*/ + ]); + + 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 new file mode 100644 index 0000000000..30f9606744 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -0,0 +1,102 @@ + + */ +class ReadOnlyPropertyRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new ReadOnlyPropertyRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80000, + [ + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 8, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 9, + ], + [ + 'Readonly property must have a native type.', + 9, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 10, + ], + [ + 'Readonly property cannot have a default value.', + 10, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 16, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 23, + ], + [ + 'Readonly property cannot be static.', + 23, + ], + ], + ], + [ + 80100, + [ + [ + 'Readonly property must have a native type.', + 9, + ], + [ + 'Readonly property cannot have a default value.', + 10, + ], + [ + 'Readonly property cannot be static.', + 23, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param list $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + $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 cd7c77688e..83c919af1f 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -2,20 +2,22 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReadingWriteOnlyPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class ReadingWriteOnlyPropertiesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false), $this->checkThisOnly); + 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 @@ -24,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, ], ]); } @@ -39,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, ], ]); } @@ -50,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, ], ]); } @@ -65,7 +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 + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/reading-write-only-properties-nullsafe.php'], [ + [ + 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', + 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/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 6b87d5b2ec..8d050e7636 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -2,17 +2,22 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class TypesAssignedToPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class TypesAssignedToPropertiesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder()); + return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, false, true), new PropertyReflectionFinder()); } public function testTypesAssignedToProperties(): void @@ -39,7 +44,7 @@ public function testTypesAssignedToProperties(): void 37, ], [ - 'Static property PropertiesAssignedTypes\Ipsum::$parentStringProperty (string) does not accept int.', + 'Property PropertiesAssignedTypes\Ipsum::$parentStringProperty (string) does not accept int.', 39, ], [ @@ -74,6 +79,49 @@ public function testTypesAssignedToProperties(): void 'Static property PropertiesAssignedTypes\Ipsum::$fooStatic (PropertiesAssignedTypes\Ipsum) does not accept PropertiesAssignedTypes\Bar.', 144, ], + [ + 'Property PropertiesAssignedTypes\AssignRefFoo::$stringProperty (string) does not accept int.', + 312, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$foo (int) does not accept int.', + 334, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$bar (int<3, max>) does not accept int<2, max>.', + 335, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$foo (int) does not accept int.', + 346, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$bar (int<3, max>) does not accept int<2, max>.', + 347, + ], + [ + 'Property PropertiesAssignedTypes\ListAssign::$foo (string) does not accept int.', + 360, + ], + [ + 'Property PropertiesAssignedTypes\AppendToArrayAccess::$collection2 (ArrayAccess&Countable) does not accept Countable.', + 376, + ], + [ + '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.', + ], ]); } @@ -82,11 +130,651 @@ 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.', + 62, + ], + ]); + } + + public function testTypesAssignedToPropertiesExpressionNames(): void + { + $this->analyse([__DIR__ . '/data/properties-from-array-into-object.php'], [ + [ + 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', + 42, + ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', + 54, + ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$test (int|null) does not accept stdClass.', + 66, + ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', + 69, + ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float.', + 83, + ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float|int|string.', + 97, + ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', + 110, + ], + [ + 'Property PropertiesFromArrayIntoObject\FooBar::$foo (string) does not accept float.', + 147, + ], + ]); + } + + public function testTypesAssignedToStaticPropertiesExpressionNames(): void + { + $this->analyse([__DIR__ . '/data/properties-from-array-into-static-object.php'], [ + [ + 'Static property PropertiesFromArrayIntoStaticObject\Foo::$lall (stdClass|null) does not accept string.', + 29, + ], + [ + 'Static property PropertiesFromArrayIntoStaticObject\Foo::$foo (string) does not accept float.', + 36, + ], + [ + 'Static property PropertiesFromArrayIntoStaticObject\FooBar::$foo (string) does not accept float.', + 72, + ], + ]); + } + + public function testBug3777(): void + { + $this->analyse([__DIR__ . '/data/bug-3777.php'], [ + [ + 'Property Bug3777\Bar::$foo (Bug3777\Foo) does not accept Bug3777\Fooo.', + 58, + ], + [ + 'Property Bug3777\Ipsum::$ipsum (Bug3777\Lorem) does not accept Bug3777\Lorem.', + 95, + ], + [ + 'Property Bug3777\Ipsum2::$lorem2 (Bug3777\Lorem2) does not accept Bug3777\Lorem2.', + 129, + ], + [ + 'Property Bug3777\Ipsum2::$ipsum2 (Bug3777\Lorem2) does not accept Bug3777\Lorem2.', + 131, + ], + [ + 'Property Bug3777\Ipsum3::$ipsum3 (Bug3777\Lorem3) does not accept Bug3777\Lorem3.', + 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.', + 28, + ], + [ + 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', + 30, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 31, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 33, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 38, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 46, + ], + [ + 'Property AppendedArrayKey\MorePreciseKey::$test (array<1|2|3, string>) does not accept non-empty-array.', + 80, + ], + [ + 'Property AppendedArrayKey\MorePreciseKey::$test (array<1|2|3, string>) does not accept non-empty-array<1|2|3|4, string>.', + 85, + ], + ]); + } + + public function testBug5372Two(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-5372_2.php'], []); + } + + public function testBug5447(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-5447.php'], []); + } + + public function testAppendedArrayItemType(): void + { + $this->analyse( + [__DIR__ . '/../Arrays/data/appended-array-item.php'], + [ + [ + 'Property AppendedArrayItem\Foo::$integers (array) does not accept array.', + 18, + ], + [ + 'Property AppendedArrayItem\Foo::$callables (array) does not accept non-empty-array.', + 20, + ], + [ + 'Property AppendedArrayItem\Foo::$callables (array) does not accept non-empty-array.', + 23, + ], + [ + 'Property AppendedArrayItem\Foo::$callables (array) does not accept non-empty-array.', + 25, + ], + [ + 'Property AppendedArrayItem\Foo::$integers (array) does not accept array.', + 27, + ], + [ + 'Property AppendedArrayItem\Foo::$integers (array) does not accept array.', + 32, + ], + [ + 'Property AppendedArrayItem\Bar::$stringCallables (array) does not accept non-empty-array<(callable(): string)|(Closure(): 1)>.', + 45, + ], + [ + 'Property AppendedArrayItem\Baz::$staticProperty (array) does not accept array.', + 79, + ], + ], + ); + } + + public function testBug5804(): void + { + $this->analyse([__DIR__ . '/data/bug-5804.php'], [ + [ + 'Property Bug5804\Blah::$value (array|null) does not accept array.', + 12, + ], + [ + 'Property Bug5804\Blah::$value (array|null) does not accept array.', + 17, + ], + ]); + } + + public function testBug6286(): void + { + $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.", + 22, + "Offset 'age' (int) does not accept type int|string.", + ], + ]); + } + + public function testBug4906(): void + { + $this->analyse([__DIR__ . '/data/bug-4906.php'], []); + } + + public function testBug4910(): void + { + $this->analyse([__DIR__ . '/data/bug-4910.php'], []); + } + + public function testBug3703(): void + { + $this->analyse([__DIR__ . '/data/bug-3703.php'], [ + [ + 'Property Bug3703\Foo::$bar (array>>) does not accept array>>.', + 15, + ], + [ + 'Property Bug3703\Foo::$bar (array>>) does not accept array|int>>.', + 18, + ], + [ + 'Property Bug3703\Foo::$bar (array>>) does not accept array>|string>.', + 21, + ], + ]); + } + + public function testBug6333(): void + { + $this->analyse([__DIR__ . '/data/bug-6333.php'], []); + } + + 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'], []); + } + + public function testGenericObjectWithUnspecifiedTemplateTypes(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Foo::$obj (GenericObjectUnspecifiedTemplateTypes\MyObject) does not accept GenericObjectUnspecifiedTemplateTypes\MyObject<(int|string), mixed>.', + 13, + ], + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', + 67, + ], + ]); + } + + public function testGenericObjectWithUnspecifiedTemplateTypesLevel8(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', + 67, + ], + ]); + } + + 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 new file mode 100644 index 0000000000..22c15b707b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -0,0 +1,238 @@ + + */ +class UninitializedPropertyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UninitializedPropertyRule( + new ConstructorsHelper( + self::getContainer(), + [ + 'UninitializedProperty\\TestCase::setUp', + 'Bug9619\\AdminPresenter::startup', + 'Bug9619\\AdminPresenter2::startup', + 'Bug9619\\AdminPresenter3::startup', + 'Bug9619\\AdminPresenter3::startup2', + ], + ), + ); + } + + protected function getReadWritePropertiesExtensions(): array + { + return [ + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + 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 static function getAdditionalConfigFiles(): array + { + 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.', + 10, + ], + [ + 'Class UninitializedProperty\Foo has an uninitialized property $baz. Give it default value or assign it in the constructor.', + 12, + ], + [ + 'Access to an uninitialized property UninitializedProperty\Bar::$foo.', + 33, + ], + [ + 'Class UninitializedProperty\Lorem has an uninitialized property $baz. Give it default value or assign it in the constructor.', + 59, + ], + [ + '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 + { + $this->analyse([__DIR__ . '/data/uninitialized-property-promoted.php'], []); + } + + public function testReadOnly(): void + { + // 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 af1384db62..b11e08f127 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -2,20 +2,22 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class WritingToReadOnlyPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class WritingToReadOnlyPropertiesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); + return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); } public function testCheckThisOnlyProperties(): void @@ -24,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, ], ]); } @@ -39,21 +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.', + 30, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 25, + 31, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 26, + 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-private-property-static.php b/tests/PHPStan/Rules/Properties/data/access-private-property-static.php new file mode 100644 index 0000000000..51e6fc3917 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/access-private-property-static.php @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000000..ddac393d08 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/access-properties-assign-op.php @@ -0,0 +1,18 @@ +flags |= 1; + } + + public function doBar() + { + $this->flags ??= 2; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-assign.php b/tests/PHPStan/Rules/Properties/data/access-properties-assign.php index 36c26da329..89f58619b5 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-assign.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-assign.php @@ -7,7 +7,7 @@ class AccessPropertyWithDimFetch public function doFoo() { - $this->foo['foo'] = 'test'; // already reported by a separate rule + $this->foo['foo'] = 'test'; } public function doBar() diff --git a/tests/PHPStan/Rules/Properties/data/access-properties.php b/tests/PHPStan/Rules/Properties/data/access-properties.php index 49b7c3cf3e..f755c42eac 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties.php @@ -2,6 +2,7 @@ namespace TestAccessProperties; +#[\AllowDynamicProperties] class FooAccessProperties { @@ -253,6 +254,7 @@ public function doFoo() } +#[\AllowDynamicProperties] class NullCoalesce { @@ -274,6 +276,7 @@ public function doFoo() } +#[\AllowDynamicProperties] class IssetPropertyInWhile { @@ -301,6 +304,7 @@ public function doFoo() } +#[\AllowDynamicProperties] class PropertyIssetOnPossibleFalse { @@ -371,6 +375,7 @@ public function doBar() } +#[\AllowDynamicProperties] class AccessInIsset { @@ -403,3 +408,35 @@ public function doFoo() } } + +class Bug1884 +{ + + function mustReport(?\stdClass $nullable): bool + { + return isset($nullable->array['key']); + } + + function mustNotReport(?\stdClass $nullable): bool + { + return isset($nullable, $nullable->array['key']); + } + +} + +class OnObjectAfterIsset +{ + + /** + * @param mixed $m + */ + public function doFoo($m): void + { + if (isset($m->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 new file mode 100644 index 0000000000..9d065b72f3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php @@ -0,0 +1,18 @@ += 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-3339.php b/tests/PHPStan/Rules/Properties/data/bug-3339.php new file mode 100644 index 0000000000..a05f4451b4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3339.php @@ -0,0 +1,19 @@ +tuple = [true, true, true]; + + for ($i = 0; $i < 3; ++$i) + { + $this->tuple[$i] = false; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3371.php b/tests/PHPStan/Rules/Properties/data/bug-3371.php new file mode 100644 index 0000000000..80f89e9869 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3371.php @@ -0,0 +1,42 @@ +errno; + $error_message = $mysqli->error; + } + if ($mysqliValid && $error_number === 0) { + $error_number = (int) $mysqli->connect_errno; + $error_message = $mysqli->connect_error; + } + + + if ($mysqli !== null && $mysqli !== false) { + $error_number = $mysqli->errno; + $error_message = $mysqli->error; + } + if ($mysqli !== null && $mysqli !== false && $error_number === 0) { + $error_number = (int) $mysqli->connect_errno; + $error_message = $mysqli->connect_error; + } + + + return 'string'; + } +} 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-3703.php b/tests/PHPStan/Rules/Properties/data/bug-3703.php new file mode 100644 index 0000000000..dd60a9382b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3703.php @@ -0,0 +1,24 @@ +> + */ + public $bar; + + public function doFoo() + { + $foo = new self(); + // Should not be allowed (missing string key) + $foo->bar['foo']['bar'][] = 'ok'; + + // Should not be allowed (value should be array) + $foo->bar['foo']['bar'] = 1; + + // Should not be allowed + $foo->bar['ok'] = 'ok'; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-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-3777.php b/tests/PHPStan/Rules/Properties/data/bug-3777.php new file mode 100644 index 0000000000..6ac99af08b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3777.php @@ -0,0 +1,172 @@ + + */ + public $dates; + + public function __construct() + { + $this->dates = new \SplObjectStorage(); + assertType('SplObjectStorage', $this->dates); + } +} + +/** @template T of object */ +class Foo +{ + + public function __construct() + { + + } + +} + +/** @template T of object */ +class Fooo +{ + +} + +class Bar +{ + + /** @var Foo<\stdClass> */ + private $foo; + + /** @var Fooo<\stdClass> */ + private $fooo; + + public function __construct() + { + $this->foo = new Foo(); + assertType('Bug3777\Foo', $this->foo); + + $this->fooo = new Fooo(); + assertType('Bug3777\Fooo', $this->fooo); + } + + public function doBar() + { + $this->foo = new Fooo(); + assertType('Bug3777\Fooo', $this->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 $lorem; + + /** @var Lorem<\stdClass, \Exception> */ + private $ipsum; + + public function __construct() + { + $this->lorem = new Lorem(new \stdClass, new \Exception()); + assertType('Bug3777\Lorem', $this->lorem); + $this->ipsum = new Lorem(new \Exception(), new \stdClass); + assertType('Bug3777\Lorem', $this->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 $lorem2; + + /** @var Lorem2<\stdClass, \Exception> */ + private $ipsum2; + + public function __construct() + { + $this->lorem2 = new Lorem2(new \stdClass); + assertType('Bug3777\Lorem2', $this->lorem2); + $this->ipsum2 = new Lorem2(new \Exception()); + assertType('Bug3777\Lorem2', $this->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 $lorem3; + + /** @var Lorem3<\stdClass, \Exception> */ + private $ipsum3; + + public function __construct() + { + $this->lorem3 = new Lorem3(new \stdClass, new \Exception()); + assertType('Bug3777\Lorem3', $this->lorem3); + $this->ipsum3 = new Lorem3(new \Exception(), new \stdClass()); + assertType('Bug3777\Lorem3', $this->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-3947.php b/tests/PHPStan/Rules/Properties/data/bug-3947.php new file mode 100644 index 0000000000..a028ef02e1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3947.php @@ -0,0 +1,14 @@ +items->children() as $groupItem) { + switch ((string)$groupItem->orderType) { + } + } + } +} 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-4527.php b/tests/PHPStan/Rules/Properties/data/bug-4527.php new file mode 100644 index 0000000000..bb2d57a792 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4527.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug4527; + +class Foo +{ + + /** + * @param Bar[] $bars + */ + public function foo(array $bars): void + { + ($bars['randomKey'] ?? null)?->bar; + } +} + +class Bar { + public $bar; +} 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-4808.php b/tests/PHPStan/Rules/Properties/data/bug-4808.php new file mode 100644 index 0000000000..938a1772b8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4808.php @@ -0,0 +1,24 @@ +x; + } +} + +class B extends A +{ + public function getPrivateProp(): bool + { + return \Closure::bind(function () { + return $this->x; + }, $this, A::class)(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4906.php b/tests/PHPStan/Rules/Properties/data/bug-4906.php new file mode 100644 index 0000000000..ecec635fd3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4906.php @@ -0,0 +1,32 @@ + + * @phpstan-var array + * @psalm-var Params + */ + private $params; +} + +class HelloWorld +{ + /** + * @var array + */ + private $connectionParameters; + + private function overrideConnectionParameters(): void + { + $overrideConnectionParameters = \Closure::bind(function (array $connectionParameters) { + foreach ($connectionParameters as $parameterKey => $parameterValue) { + $this->params[$parameterKey] = $parameterValue; + } + }, $this, Connection::class); + $overrideConnectionParameters($this->connectionParameters); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4910.php b/tests/PHPStan/Rules/Properties/data/bug-4910.php new file mode 100644 index 0000000000..62da7af9f4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4910.php @@ -0,0 +1,39 @@ + + */ + protected $faces = []; + + /** + * @param int[] $faces + * @phpstan-param list $faces + * @return $this + */ + public function setFaces(array $faces) : self{ + $uniqueFaces = []; + foreach($faces as $face){ + if($face !== Facing::NORTH && $face !== Facing::SOUTH && $face !== Facing::WEST && $face !== Facing::EAST){ + throw new \InvalidArgumentException("Facing can only be north, east, south or west"); + } + $uniqueFaces[$face] = $face; + } + + $this->faces = $uniqueFaces; + return $this; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5143.php b/tests/PHPStan/Rules/Properties/data/bug-5143.php new file mode 100644 index 0000000000..78c8572ee5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5143.php @@ -0,0 +1,18 @@ +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-5607.php b/tests/PHPStan/Rules/Properties/data/bug-5607.php new file mode 100644 index 0000000000..eff2724216 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5607.php @@ -0,0 +1,19 @@ + 'basic segment']; + + /** + * @param array $x + */ + public function mm($x): void + { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5804.php b/tests/PHPStan/Rules/Properties/data/bug-5804.php new file mode 100644 index 0000000000..ba24345e76 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5804.php @@ -0,0 +1,19 @@ +value[] = 'hello'; + } + + public function doBar() + { + $this->value[] = new Blah; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5868.php b/tests/PHPStan/Rules/Properties/data/bug-5868.php new file mode 100644 index 0000000000..4359f8b5bf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5868.php @@ -0,0 +1,37 @@ += 8.0 + +namespace Bug5868PropertyFetch; + +class Child +{ + + public ?self $child; + + public self $existingChild; + +} + +class Foo +{ + public ?Child $child; +} + +class HelloWorld +{ + + function getAttributeInNode(?Foo $node): ?Child + { + // Ok + $tmp = $node?->child; + $tmp = $node?->child?->child?->child; + $tmp = $node?->child?->existingChild->child; + $tmp = $node?->child?->existingChild->child?->existingChild; + + // Errors + $tmp = $node->child; + $tmp = $node?->child->child; + $tmp = $node?->child->existingChild->child; + $tmp = $node?->child?->existingChild->child->existingChild; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6020.php b/tests/PHPStan/Rules/Properties/data/bug-6020.php new file mode 100644 index 0000000000..183fd9303f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6020.php @@ -0,0 +1,9 @@ += 8.0 + +namespace Bug6020; + +function (): void { + $xml = new \SimpleXMLElement('Whatever'); + + $xml->foo?->bar?->baz; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-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-6117.php b/tests/PHPStan/Rules/Properties/data/bug-6117.php new file mode 100644 index 0000000000..7114703687 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6117.php @@ -0,0 +1,32 @@ + + */ + private $mappings = []; + + public function testMe(): void + { + $this->mappings[self::CATEGORY_TYPE_TWO] = new Mapping(); + + $this->mappings[(string)self::CATEGORY_TYPE_TWO] = new Mapping(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6286.php b/tests/PHPStan/Rules/Properties/data/bug-6286.php new file mode 100644 index 0000000000..4967cd8a22 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6286.php @@ -0,0 +1,24 @@ + + */ + public array $nestedDetails; + + public function doSomething(): void + { + $this->details ['name'] = 'Douglas Adams'; + $this->details ['age'] = 'Forty-two'; + + $this->nestedDetails [0] ['name'] = 'Bilbo Baggins'; + $this->nestedDetails [0] ['age'] = 'Eleventy-one'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6333.php b/tests/PHPStan/Rules/Properties/data/bug-6333.php new file mode 100644 index 0000000000..0f4d3bb02b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6333.php @@ -0,0 +1,16 @@ + + */ + public array $detectedCheat = []; + + public function test(): void + { + $this->detectedCheat["playerName"][1]++; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6356.php b/tests/PHPStan/Rules/Properties/data/bug-6356.php new file mode 100644 index 0000000000..4f5208a761 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6356.php @@ -0,0 +1,24 @@ +> */ + 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-6385.php b/tests/PHPStan/Rules/Properties/data/bug-6385.php new file mode 100644 index 0000000000..330d3fc780 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6385.php @@ -0,0 +1,53 @@ += 8.1 + +namespace Bug6385; + +use BackedEnum; +use UnitEnum; + +final class EnumValue +{ + public readonly string $name; + public readonly string $value; + + public function __construct( + BackedEnum | string $name, + BackedEnum | string $value + ) { + $this->name = $name instanceof BackedEnum ? $name->name : $name; + $this->value = $value instanceof BackedEnum ? $value->name : $value; + } +} + +enum ActualUnitEnum +{ + +} + +enum ActualBackedEnum: int +{ + +} + +class Foo +{ + + public function doFoo( + UnitEnum $unitEnum, + BackedEnum $backedEnum, + ActualUnitEnum $actualUnitEnum, + ActualBackedEnum $actualBackedEnum + ) + { + echo $unitEnum->name; + echo $unitEnum->value; + echo $backedEnum->name; + echo $backedEnum->value; + echo $actualUnitEnum->name; + echo $actualUnitEnum->value; + echo $actualBackedEnum->name; + echo $actualBackedEnum->value; + + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-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-6566.php b/tests/PHPStan/Rules/Properties/data/bug-6566.php new file mode 100644 index 0000000000..f592ec686e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6566.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug6566; + +class A { + public string $name; +} + +class B { + public string $name; +} + +class C { + +} + +/** + * @template T of A|B|C + */ +abstract class HelloWorld +{ + public function sayHelloBug(): void + { + $object = $this->getObject(); + if (!$object instanceof C) { + echo $object->name; + } + } + + /** + * @return T + */ + abstract protected function getObject(): A|B|C; +} diff --git a/tests/PHPStan/Rules/Properties/data/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 new file mode 100644 index 0000000000..2250cdd3ff --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/generic-object-unspecified-template-types.php @@ -0,0 +1,105 @@ + */ + private $obj; + + public function __construct() + { + $this->obj = new MyObject(); + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection +{ + + /** + * @param array $items + */ + public function __construct(array $items = []) + { + + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection2 +{ + + public function __construct(array $items = []) + { + + } + +} + +class Bar +{ + + /** @var ArrayCollection */ + private $ints; + + public function __construct() + { + $this->ints = new ArrayCollection(); + } + + public function doFoo() + { + $this->ints = new ArrayCollection([]); + } + + public function doBar() + { + $this->ints = new ArrayCollection(['foo', 'bar']); + } + +} + +class Baz +{ + + /** @var ArrayCollection2 */ + private $ints; + + public function __construct() + { + $this->ints = new ArrayCollection2(); + } + + public function doFoo() + { + $this->ints = new ArrayCollection2([]); + } + + public function doBar() + { + $this->ints = new ArrayCollection2(['foo', 'bar']); + } + +} + +/** + * @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/intersection-types.php b/tests/PHPStan/Rules/Properties/data/intersection-types.php new file mode 100644 index 0000000000..715551fbcf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/intersection-types.php @@ -0,0 +1,34 @@ +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 a88b32b76a..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; + } /** @@ -85,3 +88,49 @@ class Bar /** @var float */ private $dateTime; }; + +class CallableSignature +{ + + /** @var callable */ + 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 new file mode 100644 index 0000000000..cded620d17 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php @@ -0,0 +1,302 @@ += 8.1 + +namespace MissingReadOnlyPropertyAssign; + +class Foo +{ + + private readonly int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + private readonly int $unassigned; + + private readonly int $unassigned2; + + private readonly int $readBeforeAssigned; + + private readonly int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function __construct( + private readonly int $promoted, + ) + { + $this->assigned = 1; + + echo $this->readBeforeAssignedNotReadOnly; + $this->readBeforeAssignedNotReadOnly = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + + $this->doubleAssignedNotReadOnly = 1; + $this->doubleAssignedNotReadOnly = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +class BarDoubleAssignInSetter +{ + + private readonly int $foo; + + public function setFoo(int $i) + { + // reported in ReadOnlyPropertyAssignRule + $this->foo = $i; + $this->foo = $i; + } + +} + +class TestCase +{ + + private readonly int $foo; + + protected function setUp(): void + { + $this->foo = 1; + } + +} + +class AssignOp +{ + + private readonly int $foo; + + private readonly ?int $bar; + + public function __construct(int $foo) + { + $this->foo .= $foo; + + $this->bar ??= 3; + } + + +} + +class AssignRef +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = &$foo; + } + +} + +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-rule.php b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch-rule.php new file mode 100644 index 0000000000..9ce3a97f4b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch-rule.php @@ -0,0 +1,20 @@ += 8.0 + +namespace NullsafePropertyFetchRule; + +class Foo +{ + + public function doFoo( + $mixed, + ?\Exception $nullable, + \Exception $nonNullable + ): void + { + $mixed?->foo; + $nullable?->foo; + $nonNullable?->foo; + (null)?->foo; // reported by a different rule + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php new file mode 100644 index 0000000000..d2a693fe64 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php @@ -0,0 +1,32 @@ += 8.0 + +namespace NullsafePropertyFetch; + +class Foo +{ + + private $bar; + + public function doFoo(?self $selfOrNull): void + { + $selfOrNull?->bar; + $selfOrNull?->baz; + } + + public function doBar(string $string, ?string $nullableString): void + { + echo $string->bar ?? 4; + echo $nullableString->bar ?? 4; + + echo $string?->bar ?? 4; + 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 @@ + */ + protected $arrayClassStrings; + + /** @var string */ + protected $string; + +} + +class Bar extends Foo +{ + + /** @var array */ + protected $array; + + /** @var array */ + protected $arrayClassStrings; + + /** @var int */ + protected $string; + +} diff --git a/tests/PHPStan/Rules/Properties/data/overriding-property.php b/tests/PHPStan/Rules/Properties/data/overriding-property.php new file mode 100644 index 0000000000..cf0b46cd4c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/overriding-property.php @@ -0,0 +1,160 @@ + $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/promoted-properties-missing-typehint.php b/tests/PHPStan/Rules/Properties/data/promoted-properties-missing-typehint.php new file mode 100644 index 0000000000..533f61edc4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/promoted-properties-missing-typehint.php @@ -0,0 +1,19 @@ += 8.0 + +namespace PromotedPropertiesMissingTypehint; + +class Foo +{ + + /** + * @param int $baz + */ + public function __construct( + private int $foo, + /** @var int */ private $bar, + private $baz, + private $lorem, + private array $ipsum + ) { } + +} diff --git a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php index 61c22f21db..14b3ad66e4 100644 --- a/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php +++ b/tests/PHPStan/Rules/Properties/data/properties-assigned-types.php @@ -155,7 +155,7 @@ interface SomeInterface class Collection implements \IteratorAggregate { - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator([]); } @@ -286,3 +286,138 @@ public function doBar($a) } } + +/** + * @template T + */ +class Baz { + /** @var array{array} */ + private $var; + + function test(): void + { + $this->var = [[]]; + } +} + +class AssignRefFoo +{ + + /** @var string */ + private $stringProperty; + + public function doFoo() + { + $i = 1; + $this->stringProperty = &$i; + } + +} + +class PostInc +{ + + /** @var int */ + private $foo; + + /** @var int<3, max> */ + private $bar; + + public function doFoo(): void + { + $this->foo--; + $this->bar++; + } + + public function doBar(): void + { + $this->foo++; + $this->bar--; + } + + public function doFoo2(): void + { + --$this->foo; + ++$this->bar; + } + + public function doBar2(): void + { + ++$this->foo; + --$this->bar; + } + +} + +class ListAssign +{ + + /** @var string */ + private $foo; + + public function doFoo() + { + [$this->foo] = [1]; + } + +} + +class AppendToArrayAccess +{ + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } +} + +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-from-array-into-object.php b/tests/PHPStan/Rules/Properties/data/properties-from-array-into-object.php new file mode 100644 index 0000000000..95fa8c9a32 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-from-array-into-object.php @@ -0,0 +1,153 @@ +data() as $property => $value) { + $self->{$property} = $value; + } + + return $self; + } + + public function create_simple_1(): self { + $self = new self(); + + $data = $this->data(); + + foreach($data as $property => $value) { + $self->{$property} = $value; + } + + return $self; + } + + public function create_complex(): self { + $self = new self(); + + foreach($this->data() as $property => $value) { + if ($property === 'test') { + if ($self->{$property} === null) { + $self->{$property} = new \stdClass(); + } + } else { + $self->{$property} = $value; + } + + if ($property === 'foo') { + $self->{$property} += 1; + } + if ($property === 'foo') { + $self->{$property} .= ' '; + } + if ($property === 'lall') { + $self->{$property} += 1; + } + $tmp = 1.1; + if ($property === 'foo') { + $self->{$property} += $tmp; + } + } + + return $self; + } + + public function create_simple_2(): self { + $self = new self(); + + $data = $this->data(); + + $property = 'foo'; + foreach($data as $value) { + $self->{$property} = $value; + } + + return $self; + } + + public function create_double_loop(): self { + $self = new self(); + + $data = $this->data(); + + foreach($data as $property => $value) { + foreach([1, 2, 3] as $value_2) { + $self->{$property} = $value; + } + } + + return $self; + } +} + + +class FooBar +{ + /** + * @var string + */ + public $foo = ''; + + /** + * @var null|\stdClass + */ + public $lall; + + public function data(): array + { + return ['foo' => 'bar', 'lall' => 'lall', 'noop' => 1]; + } + + public function create(): self { + $self = new self(); + + foreach($this->data() as $property => $value) { + $this->{$property} = $value; + + if ($property === 'lall') { + $this->{$property} = null; + } + + if ($property === 'foo') { + $this->{$property} = 1.1; + } + } + + return $self; + } +} \ No newline at end of file diff --git a/tests/PHPStan/Rules/Properties/data/properties-from-array-into-static-object.php b/tests/PHPStan/Rules/Properties/data/properties-from-array-into-static-object.php new file mode 100644 index 0000000000..047f11d5f5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-from-array-into-static-object.php @@ -0,0 +1,78 @@ + 'bar', 'lall' => 'lall', 'noop' => 1]; + } + + public function create(): self { + $self = new self(); + + foreach($this->data() as $property => $value) { + self::${$property} = $value; + + if ($property === 'lall') { + self::${$property} = null; + } + + if ($property === 'foo') { + self::${$property} = 1.1; + } + } + + return $self; + } +} + +class FooBar +{ + /** + * @var string + */ + public static $foo = ''; + + /** + * @var null|\stdClass + */ + public static $lall; + + public function data(): array + { + return ['foo' => 'bar', 'lall' => 'lall', 'noop' => 1]; + } + + public function create(): self { + $self = new self(); + + foreach($this->data() as $property => $value) { + self::${$property} = $value; + + if ($property === 'lall') { + self::${$property} = null; + } + + if ($property === 'foo') { + self::${$property} = 1.1; + } + } + + return $self; + } +} \ No newline at end of file diff --git a/tests/PHPStan/Rules/Properties/data/properties-from-variable-into-object.php b/tests/PHPStan/Rules/Properties/data/properties-from-variable-into-object.php new file mode 100644 index 0000000000..b6e0252e16 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-from-variable-into-object.php @@ -0,0 +1,30 @@ +{$property} = $data; + + $data = 'foo'; + $property = 'noop'; + $self->{$property} = $data; + + return $self; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/properties-from-variable-into-static-object.php b/tests/PHPStan/Rules/Properties/data/properties-from-variable-into-static-object.php new file mode 100644 index 0000000000..7f6fef24a5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-from-variable-into-static-object.php @@ -0,0 +1,30 @@ +{$property} = $data; + + $data = 'foo'; + $property = 'noop'; + $self->{$property} = $data; + + return $self; + } +} \ No newline at end of file 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/properties-promoted-types.php b/tests/PHPStan/Rules/Properties/data/properties-promoted-types.php new file mode 100644 index 0000000000..1932476df4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-promoted-types.php @@ -0,0 +1,22 @@ += 8.0 + +namespace PromotedPropertiesExistingClasses; + +class Foo +{ + + public function __construct( + public \stdClass $foo, + /** @var \stdClass */ public $bar, + public SomeTrait $baz, + /** @var SomeTrait */ public $lorem, + public Bar $ipsum, + /** @var Bar */ public $dolor + ) { } + +} + +trait SomeTrait +{ + +} 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 new file mode 100644 index 0000000000..20cc6d1c0f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property.php @@ -0,0 +1,24 @@ += 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 new file mode 100644 index 0000000000..2042e0005f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php @@ -0,0 +1,11 @@ += 8.0 + +namespace ReadingWriteOnlyProperties; + +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-ref.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref.php new file mode 100644 index 0000000000..125afc984d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref.php @@ -0,0 +1,29 @@ += 8.1 + +namespace ReadOnlyPropertyAssignRef; + +class Foo +{ + + private readonly int $foo; + + public readonly int $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} + +class Bar +{ + + public function doBar(Foo $foo) + { + $a = &$foo->foo; // private + $b = &$foo->bar; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign.php b/tests/PHPStan/Rules/Properties/data/readonly-assign.php new file mode 100644 index 0000000000..e23655217c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign.php @@ -0,0 +1,215 @@ += 8.1 + +namespace ReadonlyPropertyAssign; + +class Foo +{ + + private readonly int $foo; + + protected readonly int $bar; + + public readonly int $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} + +class Bar extends Foo +{ + + public function __construct(int $bar) + { + parent::__construct(1); + $this->foo = $foo; // do not report - private property + $this->bar = $bar; // report - not in declaring class + $this->baz = $baz; // report - not in declaring class + } + + public function setBar(int $bar): void + { + $this->bar = $bar; // report - not in declaring class + } + +} + +function (Foo $foo): void { + $foo->foo = 1; // do not report - private property + $foo->baz = 2; // report - not in declaring class +}; + +class FooArrays +{ + + /** + * @var array{name:string,age:int} + */ + public readonly array $details; + + public function __construct() + { + $this->details = ['name' => 'Foo', 'age' => 25]; + } + + public function doSomething(): void + { + $this->details['name'] = 'Bob'; + $this->details['age'] = 42; + } + +} + +class NotReadonly +{ + + private int $foo; + + public function setFoo(int $foo): void + { + $this->foo = $foo; // do not report - not readonly + } + +} + +class NotThis +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $self = new self(1); + $self->foo = $foo; // report - not $this + } + +} + +class PostInc +{ + + private readonly int $foo; + + public function doFoo(): void + { + $this->foo++; + --$this->foo; + + $this->foo += 5; + } + +} + +class ListAssign +{ + + private readonly int $foo; + + public function __construct() + { + [$this->foo] = [1]; + } + + public function setFoo() + { + [$this->foo] = [1]; + } + + public function setBar() + { + list($this->foo) = [1]; + } + +} + +enum FooEnum: string +{ + + case ONE = 'one'; + case TWO = 'two'; + + public function doFoo(): void + { + $this->name = 'ONE'; + $this->value = 'one'; + } + +} + +class TestFooEnum +{ + + public function doFoo(FooEnum $foo): void + { + $foo->name = 'ONE'; + $foo->value = 'one'; + } + +} + +class AssignRefOutsideClass +{ + + public function doFoo(Foo $foo, int $i) + { + $foo->baz = 5; + $foo->baz = &$i; + } + +} + +class Unserialization +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + /** + * @param array $data + */ + public function __unserialize(array $data) : void + { + [$this->foo] = $data; // __unserialize - fine + } + +} + +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-promoted.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-promoted.php new file mode 100644 index 0000000000..ea2c55c283 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-promoted.php @@ -0,0 +1,41 @@ += 8.0 + +namespace UninitializedPropertyPromoted; + +class Foo +{ + + private int $x; + + public function __construct( + private int $y + ) + { + $this->x = $this->y; + } + +} + +class Bar +{ + + public function __construct( + private int $x + ) + { + + } + +} + +class Baz +{ + + public function __construct( + private int $x + ) + { + assert($this->x >= 0.0); + } + +} 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-readonly.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly.php new file mode 100644 index 0000000000..c47d6e765d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly.php @@ -0,0 +1,28 @@ += 8.1 + +namespace UninitializedPropertyReadonly; + +class Foo +{ + + private readonly int $bar; + + public function __construct() + { + + } + +} + +class Bar +{ + + private readonly int $bar; + + public function __construct() + { + echo $this->bar; + $this->bar = 1; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php new file mode 100644 index 0000000000..eea8ff632d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php @@ -0,0 +1,472 @@ +foo = 1; + } + + public function setBaz() + { + $this->baz = 1; + } + +} + +class Bar +{ + + private int $foo; + + public function __construct() + { + $this->foo += 1; + $this->foo = 2; + } + +} + +class Baz +{ + + private int $foo; + + public function __construct() + { + $this->foo = 2; + $this->foo += 1; + } + +} + +class Lorem +{ + + private int $foo; + + private int $bar; + + private int $baz; + + private int $lorem; + + private int $ipsum; + + private function assign() + { + $this->bar = 2; + $this->assignAgain(); + self::assignAgainAgain(); + } + + public function __construct() + { + $this->foo = 1; + $this->assign(); + } + + private function assignAgain() + { + $this->lorem = 4; + } + + private function assignAgainAgain() + { + $this->ipsum = 5; + } + + public function notCalled() + { + $this->baz = 3; + } + +} + +class TestCase +{ + + protected function setUp() + { + + } + +} + +class MyTestCase extends TestCase +{ + + private int $foo; + + protected function setUp() + { + $this->foo = 1; + } + +} + +class TestExtension +{ + + private int $inited; + + private int $uninited; + +} + +class ImplicitArrayCreation +{ + + /** @var mixed[] */ + private array $properties; + + private function __construct(string $message) + { + $this->properties['message'] = $message; + } + +} + +class ImplicitArrayCreation2 +{ + + /** @var mixed[] */ + private array $properties; + + private function __construct(string $message) + { + $this->properties['foo']['message'] = $message; + } + +} + +trait FooTrait +{ + + private int $foo; + + private int $bar; + + private int $baz; + + public function setBaz() + { + $this->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 1f4e0a63a6..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,8 +33,19 @@ public function doFoo() $self->usualProperty = 1; $self->usualProperty .= 1; + $self->asymmetricProperty = "1"; + $self->asymmetricProperty = 1; + $self->writeOnlyProperty = 1; $self->writeOnlyProperty .= 1; + + $s = 'foo'; + $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..9be4df6153 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -0,0 +1,180 @@ + + */ +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, + ], + ]); + } + + public function testBug12224(): void + { + $this->analyse([__DIR__ . '/data/bug-12224.php'], [ + [ + 'Function PHPStan\Rules\Pure\data\pureWithThrowsVoid() is marked as pure but returns void.', + 18, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php new file mode 100644 index 0000000000..a483c6d580 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -0,0 +1,223 @@ + + */ +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'], []); + } + + public function testBug12224(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12224.php'], [ + ['Method PHPStan\Rules\Pure\data\A::pureWithThrowsVoid() is marked as pure but returns void.', 47], + ]); + } + +} 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 3c53137056..99f777b2ff 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -2,217 +2,205 @@ namespace PHPStan\Rules\Regexp; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Regex\RegexExpressionHelper; +use function sprintf; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class RegularExpressionPatternRuleTest extends \PHPStan\Testing\RuleTestCase +class RegularExpressionPatternRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new RegularExpressionPatternRule(); + 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', + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart), 43, ], - [ - 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', - 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, + 43, ], [ - 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', - 33, + 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', - 35, + 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: ~(~', - 35, + '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', - 38, + 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: ~(~', - 39, + 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', - 41, - ], - [ - '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 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(\PhpParser\Node\Expr\FuncCall::class); - $this->assertCount(1, $rules); - $this->assertSame($rule, $rules[0]); - - $this->assertCount(0, $registry->getRules(\PhpParser\Node\Expr\MethodCall::class)); - } - - public function testGetRulesWithTwoDifferentInstances(): void - { - $fooRule = new UniversalRule(\PhpParser\Node\Expr\FuncCall::class, static function (\PhpParser\Node\Expr\FuncCall $node, Scope $scope): array { - return ['Foo error']; - }); - $barRule = new UniversalRule(\PhpParser\Node\Expr\FuncCall::class, static function (\PhpParser\Node\Expr\FuncCall $node, Scope $scope): array { - return ['Bar error']; - }); - - $registry = new Registry([ - $fooRule, - $barRule, - ]); - - $rules = $registry->getRules(\PhpParser\Node\Expr\FuncCall::class); - $this->assertCount(2, $rules); - $this->assertSame($fooRule, $rules[0]); - $this->assertSame($barRule, $rules[1]); - - $this->assertCount(0, $registry->getRules(\PhpParser\Node\Expr\MethodCall::class)); - } - -} 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 00b517040d..3abc8d8643 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase { @@ -18,12 +18,9 @@ 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 typehint.', + 'Anonymous function never returns null so it can be removed from the return type.', 14, ], ]); diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index 13e17f6aa6..f47472a2dd 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase { @@ -20,7 +20,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/tooWideClosureReturnType.php'], [ [ - 'Anonymous function never returns null so it can be removed from the return typehint.', + 'Anonymous function never returns null so it can be removed from the return type.', 20, ], ]); diff --git a/tests/PHPStan/Rules/TooWideTypehints/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 c86f55d91d..9c86812e92 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideFunctionReturnTypehintRuleTest extends RuleTestCase { @@ -21,13 +21,43 @@ public function testRule(): void require_once __DIR__ . '/data/tooWideFunctionReturnType.php'; $this->analyse([__DIR__ . '/data/tooWideFunctionReturnType.php'], [ [ - 'Function TooWideFunctionReturnType\bar() never returns string so it can be removed from the return typehint.', + 'Function TooWideFunctionReturnType\bar() never returns string so it can be removed from the return type.', 11, ], [ - 'Function TooWideFunctionReturnType\baz() never returns null so it can be removed from the return typehint.', + 'Function TooWideFunctionReturnType\baz() never returns null so it can be removed from the return type.', 15, ], + [ + 'Function TooWideFunctionReturnType\ipsum() never returns null so it can be removed from the return type.', + 27, + ], + [ + 'Function TooWideFunctionReturnType\dolor2() never returns null so it can be removed from the return type.', + 41, + ], + [ + 'Function TooWideFunctionReturnType\dolor4() never returns int so it can be removed from the return type.', + 59, + ], + [ + 'Function TooWideFunctionReturnType\dolor6() never returns null so it can be removed from the return type.', + 79, + ], + [ + '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 299cb76101..a98c638d24 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -4,29 +4,52 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ 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 { $this->analyse([__DIR__ . '/data/tooWideMethodReturnType-private.php'], [ [ - 'Method TooWideMethodReturnType\Foo::bar() never returns string so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Foo::bar() never returns string so it can be removed from the return type.', 14, ], [ - 'Method TooWideMethodReturnType\Foo::baz() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Foo::baz() never returns null so it can be removed from the return type.', 18, ], + [ + 'Method TooWideMethodReturnType\Foo::dolor() never returns null so it can be removed from the return type.', + 34, + ], + [ + 'Method TooWideMethodReturnType\Foo::dolor2() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method TooWideMethodReturnType\Foo::dolor4() never returns int so it can be removed from the return type.', + 66, + ], + [ + 'Method TooWideMethodReturnType\Foo::dolor6() never returns null so it can be removed from the return type.', + 86, + ], + [ + 'Method TooWideMethodReturnType\ConditionalTypeClass::conditionalType() never returns string so it can be removed from the return type.', + 119, + ], ]); } @@ -34,15 +57,15 @@ public function testPublicProtected(): void { $this->analyse([__DIR__ . '/data/tooWideMethodReturnType-public-protected.php'], [ [ - 'Method TooWideMethodReturnType\Bar::bar() never returns string so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Bar::bar() never returns string so it can be removed from the return type.', 14, ], [ - 'Method TooWideMethodReturnType\Bar::baz() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Bar::baz() never returns null so it can be removed from the return type.', 18, ], [ - 'Method TooWideMethodReturnType\Bazz::lorem() never returns string so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Bazz::lorem() never returns string so it can be removed from the return type.', 35, ], ]); @@ -52,14 +75,129 @@ public function testPublicProtectedWithInheritance(): void { $this->analyse([__DIR__ . '/data/tooWideMethodReturnType-public-protected-inheritance.php'], [ [ - 'Method TooWideMethodReturnType\Baz::baz() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Baz::baz() never returns null so it can be removed from the return type.', 27, ], [ - 'Method TooWideMethodReturnType\BarClass::doFoo() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\BarClass::doFoo() never returns null so it can be removed from the return type.', 51, ], ]); } + public function testBug5095(): void + { + $this->analyse([__DIR__ . '/data/bug-5095.php'], [ + [ + 'Method Bug5095\Parser::unaryOperatorFor() never returns \'not\' so it can be removed from the return type.', + 21, + ], + ]); + } + + public function testBug6158(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-6158.php'], []); + } + + 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-5095.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-5095.php new file mode 100644 index 0000000000..8c6781fbc4 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-5095.php @@ -0,0 +1,37 @@ + + */ +class Collection implements \ArrayAccess { + + /** @var array */ + private array $values; + + /** + * @param TKey $offset + */ + final public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->values); + } + + /** + * @param TKey $offset + * + * @return T + */ + final public function offsetGet(mixed $offset): mixed + { + return $this->values[$offset]; + } + + /** + * @param TKey|null $offset + * @param T $value + */ + final public function offsetSet($offset, $value): void + { + $this->values[$offset] = $value; + } + + /** + * @param TKey $offset + */ + final public function offsetUnset($offset): void + { + unset($this->values[$offset]); + } + + /** @return T|null */ + final public function randValue(): mixed + { + if ($this->values === []) { + return null; + } + + return $this[array_rand($this->values)]; + } +} + +final class User { + + public UserCollection $users; + + public function __construct() + { + $this->users = new UserCollection(); + } + + public function randValue(): ?User + { + return $this->rand(); + } + + private function rand(): ?User + { + return $this->users->randValue(); + } + +} + +/** + * @extends Collection + */ +class UserCollection extends Collection +{ +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-6175.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6175.php new file mode 100644 index 0000000000..f140ab6740 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6175.php @@ -0,0 +1,20 @@ +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 @@ + */ class UniversalRule implements Rule @@ -15,12 +15,12 @@ class UniversalRule implements Rule /** @phpstan-var class-string */ 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,8 +35,7 @@ public function getNodeType(): string /** * @param TNodeType $node - * @param \PHPStan\Analyser\Scope $scope - * @return array + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php b/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php index 41818c5253..ccd8f350fc 100644 --- a/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php +++ b/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php @@ -3,15 +3,15 @@ namespace PHPStan\Rules\Variables; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CompactVariablesRuleTest extends \PHPStan\Testing\RuleTestCase +class CompactVariablesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkMaybeUndefinedVariables; + private bool $checkMaybeUndefinedVariables; protected function getRule(): Rule { @@ -30,6 +30,10 @@ public function testCompactVariables(): void 'Call to function compact() contains possibly undefined variable $baz.', 23, ], + [ + 'Call to function compact() contains undefined variable $foo.', + 29, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableInAnonymousFunctionUseRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableInAnonymousFunctionUseRuleTest.php deleted file mode 100644 index f167944646..0000000000 --- a/tests/PHPStan/Rules/Variables/DefinedVariableInAnonymousFunctionUseRuleTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -class DefinedVariableInAnonymousFunctionUseRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - /** @var bool */ - private $checkMaybeUndefinedVariables; - - protected function getRule(): \PHPStan\Rules\Rule - { - return new DefinedVariableInAnonymousFunctionUseRule($this->checkMaybeUndefinedVariables); - } - - public function testDefinedVariables(): void - { - $this->checkMaybeUndefinedVariables = true; - $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ - [ - 'Variable $bar might not be defined.', - 5, - ], - [ - 'Variable $wrongErrorHandler might not be defined.', - 22, - ], - [ - 'Variable $onlyInIf might not be defined.', - 23, - ], - [ - 'Variable $forI might not be defined.', - 24, - ], - [ - 'Variable $forJ might not be defined.', - 25, - ], - [ - 'Variable $anotherVariableFromForCond might not be defined.', - 26, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 0967819bb3..71ad7280c8 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -2,32 +2,29 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DefinedVariableRuleTest extends \PHPStan\Testing\RuleTestCase +class DefinedVariableRuleTest extends RuleTestCase { - /** @var bool */ - private $cliArgumentsVariablesRegistered; - - /** @var bool */ - private $checkMaybeUndefinedVariables; + private bool $cliArgumentsVariablesRegistered; - /** @var bool */ - private $polluteScopeWithLoopInitialAssignments; + private bool $checkMaybeUndefinedVariables; - /** @var bool */ - private $polluteCatchScopeWithTryAssignments; + private bool $polluteScopeWithLoopInitialAssignments; - /** @var bool */ - private $polluteScopeWithAlwaysIterableForeach; + private bool $polluteScopeWithAlwaysIterableForeach; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DefinedVariableRule( $this->cliArgumentsVariablesRegistered, - $this->checkMaybeUndefinedVariables + $this->checkMaybeUndefinedVariables, ); } @@ -36,11 +33,6 @@ protected function shouldPolluteScopeWithLoopInitialAssignments(): bool return $this->polluteScopeWithLoopInitialAssignments; } - protected function shouldPolluteCatchScopeWithTryAssignments(): bool - { - return $this->polluteCatchScopeWithTryAssignments; - } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool { return $this->polluteScopeWithAlwaysIterableForeach; @@ -51,7 +43,6 @@ public function testDefinedVariables(): void require_once __DIR__ . '/data/defined-variables-definition.php'; $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables.php'], [ @@ -75,6 +66,10 @@ public function testDefinedVariables(): void 'Undefined variable: $parseStrParameter', 34, ], + [ + 'Undefined variable: $parseStrParameter', + 36, + ], [ 'Undefined variable: $foo', 39, @@ -107,16 +102,28 @@ public function testDefinedVariables(): void 'Undefined variable: $variableInEmpty', 145, ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 152, + ], [ 'Undefined variable: $variableInEmpty', 155, ], [ - 'Variable $negatedVariableInEmpty might not be defined.', + 'Undefined variable: $negatedVariableInEmpty', 156, ], [ - 'Variable $variableInIsset might not be defined.', + 'Undefined variable: $variableInIsset', + 159, + ], + [ + 'Undefined variable: $anotherVariableInIsset', + 159, + ], + [ + 'Undefined variable: $variableInIsset', 161, ], [ @@ -147,10 +154,6 @@ public function testDefinedVariables(): void 'Variable $forJ might not be defined.', 251, ], - [ - 'Variable $variableDefinedInTry might not be defined.', - 260, - ], [ 'Variable $variableAvailableInAllCatches might not be defined.', 266, @@ -212,23 +215,23 @@ public function testDefinedVariables(): void 360, ], [ - '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, ], [ @@ -246,7 +249,6 @@ public function testDefinedVariablesInClosures(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-closures.php'], [ @@ -261,7 +263,6 @@ public function testDefinedVariablesInShortArrayDestructuringSyntax(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-array-destructuring-short-syntax.php'], [ @@ -284,7 +285,6 @@ public function testCliArgumentsVariablesNotRegistered(): void { $this->cliArgumentsVariablesRegistered = false; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ @@ -303,7 +303,6 @@ public function testCliArgumentsVariablesRegistered(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ @@ -351,80 +350,25 @@ public function dataLoopInitialAssignments(): array /** * @dataProvider dataLoopInitialAssignments - * @param bool $polluteScopeWithLoopInitialAssignments - * @param bool $checkMaybeUndefinedVariables - * @param mixed[][] $expectedErrors + * @param list $expectedErrors */ public function testLoopInitialAssignments( bool $polluteScopeWithLoopInitialAssignments, bool $checkMaybeUndefinedVariables, - array $expectedErrors + array $expectedErrors, ): void { $this->cliArgumentsVariablesRegistered = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); } - public function dataCatchScopePollutedWithTryAssignments(): array - { - return [ - [ - false, - false, - [], - ], - [ - false, - true, - [ - [ - 'Variable $variableInTry might not be defined.', - 6, - ], - ], - ], - [ - true, - false, - [], - ], - [ - true, - true, - [], - ], - ]; - } - - /** - * @dataProvider dataCatchScopePollutedWithTryAssignments - * @param bool $polluteCatchScopeWithTryAssignments - * @param bool $checkMaybeUndefinedVariables - * @param mixed[][] $expectedErrors - */ - public function testCatchScopePollutedWithTryAssignments( - bool $polluteCatchScopeWithTryAssignments, - bool $checkMaybeUndefinedVariables, - array $expectedErrors - ): void - { - $this->cliArgumentsVariablesRegistered = false; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments; - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/catch-scope-polluted-with-try-assignments.php'], $expectedErrors); - } - public function testDefineVariablesInClass(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/define-variables-class.php'], []); @@ -434,7 +378,6 @@ public function testDeadBranches(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/dead-branches.php'], [ @@ -465,7 +408,6 @@ public function testForeach(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/foreach.php'], [ @@ -495,27 +437,27 @@ public function testForeach(): void ], [ 'Undefined variable: $val', - 171, + 200, ], [ 'Undefined variable: $test', - 172, + 201, ], [ 'Undefined variable: $val', - 187, + 216, ], [ 'Undefined variable: $test', - 188, + 217, ], [ 'Variable $val might not be defined.', - 217, + 246, ], [ 'Variable $test might not be defined.', - 218, + 247, ], ]); } @@ -631,14 +573,12 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array /** * @dataProvider dataForeachPolluteScopeWithAlwaysIterableForeach * - * @param bool $polluteScopeWithAlwaysIterableForeach - * @param mixed[] $errors + * @param list $errors */ public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); @@ -648,7 +588,6 @@ public function testBooleanOperatorsTruthyFalsey(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/boolean-op-truthy-falsey.php'], [ @@ -665,13 +604,8 @@ 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->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-arrow-functions.php'], [ @@ -688,13 +622,8 @@ 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->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-coalesce-assign.php'], [ @@ -709,7 +638,6 @@ public function testBug2748(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-2748.php'], [ @@ -728,7 +656,6 @@ public function testGlobalVariables(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/global-variables.php'], []); @@ -738,10 +665,525 @@ public function testRootScopeMaybeDefined(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = false; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], []); } + public function testRootScopeMaybeDefinedCheck(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], [ + [ + 'Variable $maybe might not be defined.', + 3, + ], + [ + 'Variable $this might not be defined.', + 5, + ], + ]); + } + + public function testFormerThisVariableRule(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/this.php'], [ + [ + 'Undefined variable: $this', + 16, + ], + [ + 'Undefined variable: $this', + 20, + ], + [ + 'Undefined variable: $this', + 26, + ], + [ + 'Undefined variable: $this', + 38, + ], + ]); + } + + public function testClosureUse(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ + [ + 'Variable $bar might not be defined.', + 5, + ], + [ + 'Variable $wrongErrorHandler might not be defined.', + 22, + ], + [ + 'Variable $onlyInIf might not be defined.', + 23, + ], + [ + 'Variable $forI might not be defined.', + 24, + ], + [ + 'Variable $forJ might not be defined.', + 25, + ], + [ + 'Variable $anotherVariableFromForCond might not be defined.', + 26, + ], + ]); + } + + public function testNullsafeIsset(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); + } + + public function testBug1306(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-1306.php'], []); + } + + public function testBug3515(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-3515.php'], [ + [ + 'Undefined variable: $anArray', + 19, + ], + [ + 'Undefined variable: $anArray', + 20, + ], + ]); + } + + public function testBug4412(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-4412.php'], [ + [ + 'Undefined variable: $a', + 17, + ], + ]); + } + + public function testBug3283(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-3283.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/first-class-callables.php'], [ + [ + 'Undefined variable: $foo', + 10, + ], + [ + 'Undefined variable: $foo', + 11, + ], + [ + 'Undefined variable: $foo', + 29, + ], + [ + 'Undefined variable: $foo', + 30, + ], + [ + 'Undefined variable: $foo', + 48, + ], + [ + 'Undefined variable: $foo', + 49, + ], + ]); + } + + public function testBug6112(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-6112.php'], []); + } + + 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 new file mode 100644 index 0000000000..178101b951 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -0,0 +1,234 @@ + + */ +class EmptyRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + return new EmptyRule(new IssetCheck( + new PropertyDescriptor(), + new PropertyReflectionFinder(), + true, + $this->treatPhpDocTypesAsCertain, + )); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; + } + + public function testRule(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/empty-rule.php'], [ + [ + '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{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{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() always exists and is not falsy.', + 25, + ], + [ + 'Offset 0 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is always falsy.', + 36, + ], + [ + 'Offset 1 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is always falsy.', + 37, + ], + [ + 'Offset 2 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is not falsy.', + 38, + ], + [ + 'Variable $a in empty() is never defined.', + 44, + ], + [ + 'Variable $b in empty() always exists and is not falsy.', + 47, + ], + ]); + } + + public function testBug970(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-970.php'], [ + [ + 'Variable $ar in empty() is never defined.', + 9, + ], + ]); + } + + 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 0420deec70..c033e020f5 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,32 +15,62 @@ class IssetRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain; + protected function getRule(): Rule { - return new IssetRule(new IssetCheck(new PropertyDescriptor(), new PropertyReflectionFinder())); + return new IssetRule(new IssetCheck( + new PropertyDescriptor(), + new PropertyReflectionFinder(), + true, + $this->treatPhpDocTypesAsCertain, + )); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testRule(): void { + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/isset.php'], [ [ 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', 32, ], [ - 'Offset \'string\' on array(1, 2, 3) in isset() does not exist.', + 'Variable $scalar in isset() always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} in isset() does not exist.', 45, ], [ - 'Offset \'string\' on array(array(1), array(2), array(3)) in isset() does not exist.', + 'Offset \'string\' on array{array{1}, array{2}, array{3}} in isset() does not exist.', 49, ], [ - 'Offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()) in isset() always exists and is not nullable.', + 'Variable $doesNotExist in isset() is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() always exists and is not nullable.', 67, ], [ - 'Offset \'b\' on array() in isset() does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} in isset() does not exist.', 79, ], [ @@ -62,6 +93,97 @@ public function testRule(): void 'Static property IssetRule\FooCoalesce::$staticAlwaysNull (null) in isset() is always null.', 97, ], + [ + 'Variable $a in isset() always exists and is always null.', + 111, + ], + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 116, + ], + [ + 'Property IssetRule\FooCoalesce::$alwaysNull (null) in isset() is always null.', + 118, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticAlwaysNull (null) in isset() is always null.', + 123, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticString (string) in isset() is not nullable.', + 124, + ], + [ + 'Offset \'foo\' on array{foo: string} in isset() always exists and is not nullable.', + 170, + ], + [ + 'Offset \'bar\' on array{bar: 1} in isset() always exists and is not nullable.', + 173, + ], + ]); + } + + public function testRuleWithoutTreatPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/isset.php'], [ + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 32, + ], + [ + 'Variable $scalar in isset() always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} in isset() does not exist.', + 45, + ], + [ + 'Offset \'string\' on array{array{1}, array{2}, array{3}} in isset() does not exist.', + 49, + ], + [ + 'Variable $doesNotExist in isset() is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() always exists and is not nullable.', + 67, + ], + [ + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} in isset() does not exist.', + 79, + ], + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 85, + ], + [ + 'Property IssetRule\FooCoalesce::$alwaysNull (null) in isset() is always null.', + 87, + ], + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 89, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticString (string) in isset() is not nullable.', + 95, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticAlwaysNull (null) in isset() is always null.', + 97, + ], + [ + 'Variable $a in isset() always exists and is always null.', + 111, + ], [ 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', 116, @@ -83,9 +205,7 @@ public function testRule(): 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'], [ /*[ // no way to achieve this with current PHP Reflection API @@ -101,4 +221,277 @@ public function testNativePropertyTypes(): void ]); } + public function testBug4290(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4290.php'], []); + } + + public function testBug4671(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4671.php'], []); + } + + public function testVariableCertaintyInIsset(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/variable-certainty-isset.php'], [ + [ + 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', + 14, + ], + [ + 'Variable $neverDefinedVariable in isset() is never defined.', + 22, + ], + [ + 'Variable $anotherNeverDefinedVariable in isset() is never defined.', + 42, + ], + [ + 'Variable $yetAnotherNeverDefinedVariable in isset() is never defined.', + 46, + ], + [ + 'Variable $yetYetAnotherNeverDefinedVariableInIsset in isset() is never defined.', + 56, + ], + [ + 'Variable $anotherVariableInDoWhile in isset() always exists and is not nullable.', + 104, + ], + [ + 'Variable $variableInSecondCase in isset() is never defined.', + 110, + ], + [ + 'Variable $variableInFirstCase in isset() always exists and is not nullable.', + 112, + ], + [ + // could be Variable $variableInFirstCase in isset() always exists and is not nullable. + 'Variable $variableInFirstCase in isset() is never defined.', + 116, + ], + [ + // could be Variable $variableInSecondCase in isset() always exists and is not nullable. + 'Variable $variableInSecondCase in isset() is never defined.', + 117, + ], + [ + 'Variable $variableAssignedInSecondCase in isset() is never defined.', + 119, + ], + [ + 'Variable $alwaysDefinedForSwitchCondition in isset() always exists and is not nullable.', + 139, + ], + [ + 'Variable $alwaysDefinedForCaseNodeCondition in isset() always exists and is not nullable.', + 140, + ], + [ + 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', + 152, + ], + [ + 'Variable $neverDefinedVariable in isset() is never defined.', + 152, + ], + [ + 'Variable $a in isset() always exists and is not nullable.', + 214, + ], + [ + 'Variable $null in isset() always exists and is always null.', + 225, + ], + ]); + } + + public function testIssetInGlobalScope(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-global-scope.php'], [ + [ + 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', + 8, + ], + ]); + } + + public function testNullsafe(): void + { + $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__ . '/../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 a760701b4f..ba73fbe2fb 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -5,39 +5,72 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NullCoalesceRuleTest extends \PHPStan\Testing\RuleTestCase +class NullCoalesceRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + return new NullCoalesceRule(new IssetCheck( + new PropertyDescriptor(), + new PropertyReflectionFinder(), + true, + $this->treatPhpDocTypesAsCertain, + )); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool { - return new NullCoalesceRule(new IssetCheck(new PropertyDescriptor(), new PropertyReflectionFinder())); + return $this->treatPhpDocTypesAsCertain; + } + + public function shouldNarrowMethodScopeFromConstructor(): bool + { + return true; } public function testCoalesceRule(): void { - $this->analyse([__DIR__ . '/data/null-coalesce.php'], [ + $this->treatPhpDocTypesAsCertain = true; + $errors = [ [ 'Property CoalesceRule\FooCoalesce::$string (string) on left side of ?? is not nullable.', 32, ], [ - 'Offset \'string\' on array(1, 2, 3) on left side of ?? does not exist.', + 'Variable $scalar on left side of ?? always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} on left side of ?? does not exist.', 45, ], [ - 'Offset \'string\' on array(array(1), array(2), array(3)) on left side of ?? does not exist.', + 'Offset \'string\' on array{array{1}, array{2}, array{3}} on left side of ?? does not exist.', 49, ], [ - 'Offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()) on left side of ?? always exists and is not nullable.', + 'Variable $doesNotExist on left side of ?? is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ?? always exists and is not nullable.', 67, ], [ - 'Offset \'b\' on array() on left side of ?? does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ?? does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} on left side of ?? does not exist.', 79, ], [ @@ -64,6 +97,10 @@ public function testCoalesceRule(): void 'Static property CoalesceRule\FooCoalesce::$staticAlwaysNull (null) on left side of ?? is always null.', 101, ], + [ + 'Variable $a on left side of ?? always exists and is always null.', + 115, + ], [ 'Property CoalesceRule\FooCoalesce::$string (string) on left side of ?? is not nullable.', 120, @@ -88,38 +125,58 @@ public function testCoalesceRule(): void 'Static property CoalesceRule\FooCoalesce::$staticString (string) on left side of ?? is not nullable.', 131, ], - [ + ]; + if (PHP_VERSION_ID < 80100) { + $errors[] = [ 'Property ReflectionClass::$name (class-string) on left side of ?? is not nullable.', 136, - ], - ]); + ]; + } + $errors[] = [ + 'Variable $foo on left side of ?? is never defined.', + 141, + ]; + $errors[] = [ + 'Variable $bar on left side of ?? is never defined.', + 143, + ]; + $this->analyse([__DIR__ . '/data/null-coalesce.php'], $errors); } public function testCoalesceAssignRule(): void { - 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'], [ [ 'Property CoalesceAssignRule\FooCoalesce::$string (string) on left side of ??= is not nullable.', 32, ], [ - 'Offset \'string\' on array(1, 2, 3) on left side of ??= does not exist.', + 'Variable $scalar on left side of ??= always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} on left side of ??= does not exist.', 45, ], [ - 'Offset \'string\' on array(array(1), array(2), array(3)) on left side of ??= does not exist.', + 'Offset \'string\' on array{array{1}, array{2}, array{3}} on left side of ??= does not exist.', 49, ], [ - 'Offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()) on left side of ??= always exists and is not nullable.', + 'Variable $doesNotExist on left side of ??= is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ??= always exists and is not nullable.', 67, ], [ - 'Offset \'b\' on array() on left side of ??= does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 0|1, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ??= does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} on left side of ??= does not exist.', 79, ], [ @@ -142,6 +199,177 @@ public function testCoalesceAssignRule(): void 'Static property CoalesceAssignRule\FooCoalesce::$staticAlwaysNull (null) on left side of ??= is always null.', 101, ], + [ + 'Variable $a on left side of ??= always exists and is always null.', + 115, + ], + ]); + } + + public function testNullsafe(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/null-coalesce-nullsafe.php'], []); + } + + public function testVariableCertaintyInNullCoalesce(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/variable-certainty-null.php'], [ + [ + 'Variable $scalar on left side of ?? always exists and is not nullable.', + 6, + ], + [ + 'Variable $doesNotExist on left side of ?? is never defined.', + 8, + ], + [ + 'Variable $a on left side of ?? always exists and is always null.', + 13, + ], + ]); + } + + public function testVariableCertaintyInNullCoalesceAssign(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/variable-certainty-null-assign.php'], [ + [ + 'Variable $scalar on left side of ??= always exists and is not nullable.', + 6, + ], + [ + 'Variable $doesNotExist on left side of ??= is never defined.', + 8, + ], + [ + 'Variable $a on left side of ??= always exists and is always null.', + 13, + ], + ]); + } + + public function testNullCoalesceInGlobalScope(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/null-coalesce-global-scope.php'], [ + [ + 'Variable $bar on left side of ?? always exists and is not nullable.', + 6, + ], + ]); + } + + 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/ThisVariableRuleTest.php b/tests/PHPStan/Rules/Variables/ThisVariableRuleTest.php deleted file mode 100644 index b5eacf618d..0000000000 --- a/tests/PHPStan/Rules/Variables/ThisVariableRuleTest.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ -class ThisVariableRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\Rule - { - return new ThisVariableRule(); - } - - public function testReturnTypeRule(): void - { - $this->analyse([__DIR__ . '/data/this.php'], [ - [ - 'Using $this in static method ThisVariable\Foo::doBar().', - 16, - ], - [ - 'Using $this in static method ThisVariable\Foo::doBar().', - 20, - ], - [ - 'Using $this outside a class.', - 26, - ], - [ - 'Using $this in static method class@anonymous/tests/PHPStan/Rules/Variables/data/this.php:29::doBar().', - 38, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php deleted file mode 100644 index 3ffaad172d..0000000000 --- a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -class ThrowTypeRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\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, - ], - ] - ); - } - - public function testClassExists(): void - { - $this->analyse([__DIR__ . '/data/throw-class-exists.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index c4ac52f589..df34c15d50 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -2,15 +2,25 @@ 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 \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class UnsetRuleTest extends \PHPStan\Testing\RuleTestCase +class UnsetRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new UnsetRule(); + return new UnsetRule( + self::getContainer()->getByType(PropertyReflectionFinder::class), + self::getContainer()->getByType(PhpVersion::class), + ); } public function testUnsetRule(): void @@ -33,10 +43,6 @@ public function testUnsetRule(): void 'Cannot unset offset \'c\' on 1.', 18, ], - [ - 'Cannot unset offset \'b\' on 1.', - 18, - ], [ 'Cannot unset offset \'string\' on iterable.', 31, @@ -53,4 +59,165 @@ public function testBug2752(): void $this->analyse([__DIR__ . '/data/bug-2752.php'], []); } + 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/VariableCertaintyInIssetRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCertaintyInIssetRuleTest.php deleted file mode 100644 index 18854c3b5b..0000000000 --- a/tests/PHPStan/Rules/Variables/VariableCertaintyInIssetRuleTest.php +++ /dev/null @@ -1,100 +0,0 @@ - - */ -class VariableCertaintyInIssetRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\Rule - { - return new VariableCertaintyInIssetRule(); - } - - public function testVariableCertaintyInIsset(): void - { - $this->analyse([__DIR__ . '/data/variable-certainty-isset.php'], [ - [ - 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', - 14, - ], - [ - 'Variable $neverDefinedVariable in isset() is never defined.', - 22, - ], - [ - 'Variable $anotherNeverDefinedVariable in isset() is never defined.', - 42, - ], - [ - 'Variable $yetAnotherNeverDefinedVariable in isset() is never defined.', - 46, - ], - [ - 'Variable $yetYetAnotherNeverDefinedVariableInIsset in isset() is never defined.', - 56, - ], - [ - 'Variable $anotherVariableInDoWhile in isset() always exists and is not nullable.', - 104, - ], - [ - 'Variable $variableInSecondCase in isset() is never defined.', - 110, - ], - [ - 'Variable $variableInFirstCase in isset() always exists and is not nullable.', - 112, - ], - [ - 'Variable $variableInFirstCase in isset() always exists and is not nullable.', - 116, - ], - [ - 'Variable $variableInSecondCase in isset() always exists and is not nullable.', - 117, - ], - [ - 'Variable $variableAssignedInSecondCase in isset() is never defined.', - 119, - ], - [ - 'Variable $alwaysDefinedForSwitchCondition in isset() always exists and is not nullable.', - 139, - ], - [ - 'Variable $alwaysDefinedForCaseNodeCondition in isset() always exists and is not nullable.', - 140, - ], - [ - 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', - 152, - ], - [ - 'Variable $neverDefinedVariable in isset() is never defined.', - 152, - ], - [ - 'Variable $a in isset() always exists and is not nullable.', - 214, - ], - [ - 'Variable $null in isset() is always null.', - 225, - ], - ]); - } - - public function testIssetInGlobalScope(): void - { - $this->analyse([__DIR__ . '/data/isset-global-scope.php'], [ - [ - 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', - 8, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Variables/VariableCertaintyNullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCertaintyNullCoalesceRuleTest.php deleted file mode 100644 index c9c7fe7b22..0000000000 --- a/tests/PHPStan/Rules/Variables/VariableCertaintyNullCoalesceRuleTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -class VariableCertaintyNullCoalesceRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\Rule - { - return new VariableCertaintyNullCoalesceRule(); - } - - public function testVariableCertaintyInNullCoalesce(): void - { - $this->analyse([__DIR__ . '/data/variable-certainty-null.php'], [ - [ - 'Variable $scalar on left side of ?? always exists and is not nullable.', - 6, - ], - [ - 'Variable $doesNotExist on left side of ?? is never defined.', - 8, - ], - [ - 'Variable $a on left side of ?? is always null.', - 13, - ], - ]); - } - - public function testVariableCertaintyInNullCoalesceAssign(): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - - $this->analyse([__DIR__ . '/data/variable-certainty-null-assign.php'], [ - [ - 'Variable $scalar on left side of ??= always exists and is not nullable.', - 6, - ], - [ - 'Variable $doesNotExist on left side of ??= is never defined.', - 8, - ], - [ - 'Variable $a on left side of ??= is always null.', - 13, - ], - ]); - } - - public function testNullCoalesceInGlobalScope(): void - { - $this->analyse([__DIR__ . '/data/null-coalesce-global-scope.php'], [ - [ - 'Variable $bar on left side of ?? always exists and is not nullable.', - 6, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php index ab47ab9b44..d26101b421 100644 --- a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php +++ b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php @@ -2,17 +2,20 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class VariableCloningRuleTest extends \PHPStan\Testing\RuleTestCase +class VariableCloningRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, false, true)); } public function testClone(): void @@ -35,9 +38,28 @@ 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', + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/variable-cloning-nullsafe.php'], [ + [ + 'Cannot clone stdClass|null.', + 11, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/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-1306.php b/tests/PHPStan/Rules/Variables/data/bug-1306.php new file mode 100644 index 0000000000..d073514343 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-1306.php @@ -0,0 +1,12 @@ + 5) { + $user = new \stdClass; + $user->name = 'Thibaud'; + } + + echo $user->name ?? 'Default'; +}; 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-3515.php b/tests/PHPStan/Rules/Variables/data/bug-3515.php new file mode 100644 index 0000000000..da9a2c1242 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-3515.php @@ -0,0 +1,21 @@ + '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 @@ +fields = [ + 'foo' => 'bar', + 'some' => 'what', + ]; + } +} + +class ChildClass extends BaseClass +{ + public function populateFields(): void + { + if (empty($this->fields)) { + parent::populateFields(); + + unset($this->fields['foo']); + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-4290.php b/tests/PHPStan/Rules/Variables/data/bug-4290.php new file mode 100644 index 0000000000..fd06e3e0bb --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4290.php @@ -0,0 +1,30 @@ + isset($array['status']) ? $array['status'] : null, + 'value' => isset($array['value']) ? $array['value'] : null, + ]); + + if (count($data) === 0) { + return; + } + + isset($data['status']) ? 1 : 0; + } + + /** + * @return string[] + */ + public static function getArray(): array + { + return ['value' => '100']; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-4412.php b/tests/PHPStan/Rules/Variables/data/bug-4412.php new file mode 100644 index 0000000000..a8fd1970fe --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4412.php @@ -0,0 +1,20 @@ + $a */ + public $a; + + /** + * @phpstan-return T + */ + public function get() { + return $a->get(); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-4671.php b/tests/PHPStan/Rules/Variables/data/bug-4671.php new file mode 100644 index 0000000000..beba71f7a5 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4671.php @@ -0,0 +1,17 @@ + $strings + */ + public function doFoo(int $intput, array $strings): void + { + if (isset($strings[(string) $intput])) { + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-5266.php b/tests/PHPStan/Rules/Variables/data/bug-5266.php new file mode 100644 index 0000000000..728efddb86 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-5266.php @@ -0,0 +1,23 @@ + $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/bug-970.php b/tests/PHPStan/Rules/Variables/data/bug-970.php new file mode 100644 index 0000000000..c1b988f68e --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-970.php @@ -0,0 +1,14 @@ += 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 4583b5e2d6..4b2ec40f04 100644 --- a/tests/PHPStan/Rules/Variables/data/defined-variables.php +++ b/tests/PHPStan/Rules/Variables/data/defined-variables.php @@ -143,7 +143,7 @@ function () use (&$variablePassedByReferenceToClosure) { echo $variablePassedByReferenceToClosure; if (empty($variableInEmpty) && empty($anotherVariableInEmpty['foo'])) { echo $variableInEmpty; // does not exist here - return; + //return; } else { //echo $variableInEmpty; // exists here - not yet supported } @@ -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; } @@ -252,7 +252,7 @@ function () { try { $variableDefinedInTry = 1; - $variableDefinedInTryAndAllCatches = 1; + $variableDefinedInTryAndAllCatches = 1; maybeThrow(); } catch (\FooException $e) { $variableDefinedInTryAndAllCatches = 1; $variableAvailableInAllCatches = 1; @@ -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/empty-rule.php b/tests/PHPStan/Rules/Variables/data/empty-rule.php new file mode 100644 index 0000000000..169cb421ff --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/empty-rule.php @@ -0,0 +1,59 @@ += 8.1 + +namespace FirstClassCallablesDefinedVariables; + +class Foo +{ + + public function doFoo(): void + { + $foo->doFoo(); + $foo->doFoo(...); + } + + public function doBar(object $o): void + { + $o->doFoo(...); + ($p = $o)->doFoo(...); + $p->doFoo(); + $p->doFoo(...); + } + +} + +class Bar +{ + + public function doFoo(): void + { + $foo::doFoo(); + $foo::doFoo(...); + } + + public function doBar(object $o): void + { + $o::doFoo(...); + ($p = $o)::doFoo(...); + $p::doFoo(); + $p::doFoo(...); + } + +} + +class Baz +{ + + public function doFoo(): void + { + $foo(); + $foo(...); + } + + public function doBar(object $o): void + { + $o(...); + ($p = $o)(...); + $p(); + $p(...); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/foreach.php b/tests/PHPStan/Rules/Variables/data/foreach.php index 7d145465d8..7b050056f5 100644 --- a/tests/PHPStan/Rules/Variables/data/foreach.php +++ b/tests/PHPStan/Rules/Variables/data/foreach.php @@ -107,6 +107,35 @@ function (array $arr) { }; +function (array $arr) { + + if (sizeof($arr) === 0) { + return; + } + + foreach ($arr as $val) { + $test = 1; + } + + echo $val; + echo $test; + +}; + +function (array $arr) { + + if (sizeof($arr) === 0) { + return; + } + + if ($arr) { + $test = 1; + } + + echo $test; + +}; + /*function (array $arr) { if (count($arr) > 0) { diff --git a/tests/PHPStan/Rules/Variables/data/isset-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 += 8.0 + +namespace IssetNullsafe; + +function () { + if (rand(0, 2)) { + $foo = 'blabla'; + } + + if (isset($foo?->bla)) { + + } +}; diff --git a/tests/PHPStan/Rules/Variables/data/isset-object-shapes.php b/tests/PHPStan/Rules/Variables/data/isset-object-shapes.php new file mode 100644 index 0000000000..e15b2b109b --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-object-shapes.php @@ -0,0 +1,27 @@ +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/isset.php b/tests/PHPStan/Rules/Variables/data/isset.php index 508bbac2ce..b937d47f97 100644 --- a/tests/PHPStan/Rules/Variables/data/isset.php +++ b/tests/PHPStan/Rules/Variables/data/isset.php @@ -160,3 +160,15 @@ function numericStringOffset(string $code): string throw new \RuntimeException(); } + +/** + * @param array{foo: string} $array + * @param 'bar' $bar + */ +function offsetFromPhpdoc(array $array, string $bar) +{ + echo isset($array['foo']) ? $array['foo'] : 0; + + $array = ['bar' => 1]; + echo isset($array[$bar]) ? $array[$bar] : 0; +} diff --git a/tests/PHPStan/Rules/Variables/data/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 += 8.0 + +namespace NullCoalesceNullsafe; + +class Foo +{ + + public function doFoo( + $mixed, + \Exception $nonNullable, + ?\Exception $nullable + ) + { + $mixed?->foo; + $nonNullable?->foo; + $nullable?->foo; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/null-coalesce.php b/tests/PHPStan/Rules/Variables/data/null-coalesce.php index a694fa67aa..24d71e8ea0 100644 --- a/tests/PHPStan/Rules/Variables/data/null-coalesce.php +++ b/tests/PHPStan/Rules/Variables/data/null-coalesce.php @@ -136,3 +136,9 @@ function (\ReflectionClass $ref): void { echo $ref->name ?? 'foo'; echo $ref->nonexistent ?? 'bar'; }; + +function (): void { + echo $foo ?? 'foo'; + + echo $bar->bar ?? 'foo'; +}; diff --git a/tests/PHPStan/Rules/Variables/data/parameter-out-assigned-type.php b/tests/PHPStan/Rules/Variables/data/parameter-out-assigned-type.php new file mode 100644 index 0000000000..f82a3267d8 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/parameter-out-assigned-type.php @@ -0,0 +1,69 @@ +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/root-scope-maybe.php b/tests/PHPStan/Rules/Variables/data/root-scope-maybe.php index 58c84f167b..9bfaf528a8 100644 --- a/tests/PHPStan/Rules/Variables/data/root-scope-maybe.php +++ b/tests/PHPStan/Rules/Variables/data/root-scope-maybe.php @@ -1,3 +1,5 @@ test; - $foo->test; + } public static function doBar() { $this->test; - $foo->test; - $$bar->test; + + $this->blabla = 'fooo'; } 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-isset.php b/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php index 5fe21d4332..d96689ed04 100644 --- a/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php +++ b/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php @@ -1,5 +1,5 @@ = 7.4 += 8.0 + +namespace VariableCloningNullsafe; + +class Bar +{ + public \stdClass $foo; +} + +function doFoo(?Bar $bar) { + clone $bar?->foo; +}; diff --git a/tests/PHPStan/Rules/Variables/data/variable-cloning.php b/tests/PHPStan/Rules/Variables/data/variable-cloning.php index ae58c62b9b..f40a61658a 100644 --- a/tests/PHPStan/Rules/Variables/data/variable-cloning.php +++ b/tests/PHPStan/Rules/Variables/data/variable-cloning.php @@ -29,4 +29,8 @@ class Foo {}; /** @var object $object */ $object = doFoo(); clone $object; + + /** @var Bar $unknownObject */ + $unknownObject = doBaz(); + clone $unknownObject; }; diff --git a/tests/PHPStan/Rules/Variables/data/variable-nullsafe-isset.php b/tests/PHPStan/Rules/Variables/data/variable-nullsafe-isset.php new file mode 100644 index 0000000000..41a62b5925 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/variable-nullsafe-isset.php @@ -0,0 +1,24 @@ += 8.0 + +namespace VariableNullsafeIsset; + +function (): void { + if (rand(0, 2)) { + $foo = 'blabla'; + } + + if (isset($foo->bla)) { + + } +}; + + +function (): void { + if (rand(0, 2)) { + $foo = 'blabla'; + } + + if (isset($foo?->bla)) { + + } +}; diff --git a/tests/PHPStan/Rules/WarningEmittingRuleTest.php b/tests/PHPStan/Rules/WarningEmittingRuleTest.php new file mode 100644 index 0000000000..f86b6e0e23 --- /dev/null +++ b/tests/PHPStan/Rules/WarningEmittingRuleTest.php @@ -0,0 +1,48 @@ + + */ +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/Whitespace/FileWhitespaceRuleTest.php b/tests/PHPStan/Rules/Whitespace/FileWhitespaceRuleTest.php new file mode 100644 index 0000000000..b379a0c9f5 --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/FileWhitespaceRuleTest.php @@ -0,0 +1,59 @@ + + */ +class FileWhitespaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FileWhitespaceRule(); + } + + public function testBom(): void + { + $this->analyse([__DIR__ . '/data/bom.php'], [ + [ + 'File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.', + 1, + ], + ]); + } + + public function testCorrectFile(): void + { + $this->analyse([__DIR__ . '/data/correct.php'], []); + } + + public function testTrailingWhitespaceWithoutNamespace(): void + { + $this->analyse([__DIR__ . '/data/trailing.php'], [ + [ + '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.', + 6, + ], + ]); + } + + public function testTrailingWhitespace(): void + { + $this->analyse([__DIR__ . '/data/trailing-namespace.php'], [ + [ + '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.', + 8, + ], + ]); + } + + public function testHtmlAfterClose(): void + { + $this->analyse([__DIR__ . '/data/html-after-close.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Whitespace/data/bom.php b/tests/PHPStan/Rules/Whitespace/data/bom.php new file mode 100644 index 0000000000..d17f0c1edf --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/data/bom.php @@ -0,0 +1,3 @@ + + + diff --git a/tests/PHPStan/Rules/Whitespace/data/trailing-namespace.php b/tests/PHPStan/Rules/Whitespace/data/trailing-namespace.php new file mode 100644 index 0000000000..6aa51ab137 --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/data/trailing-namespace.php @@ -0,0 +1,8 @@ + + diff --git a/tests/PHPStan/Rules/Whitespace/data/trailing.php b/tests/PHPStan/Rules/Whitespace/data/trailing.php new file mode 100644 index 0000000000..37f79ec490 --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/data/trailing.php @@ -0,0 +1,6 @@ + + diff --git a/tests/PHPStan/Rules/data/datetime-instantiation.php b/tests/PHPStan/Rules/data/datetime-instantiation.php new file mode 100644 index 0000000000..47bb5deb95 --- /dev/null +++ b/tests/PHPStan/Rules/data/datetime-instantiation.php @@ -0,0 +1,23 @@ +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/Tests/AssertionClass.php b/tests/PHPStan/Tests/AssertionClass.php index 589993afc2..29f419fcad 100644 --- a/tests/PHPStan/Tests/AssertionClass.php +++ b/tests/PHPStan/Tests/AssertionClass.php @@ -2,33 +2,37 @@ namespace PHPStan\Tests; +use function is_int; + class AssertionClass { + /** @throws AssertionException */ public function assertString(?string $arg): bool { if ($arg === null) { - throw new \Exception(); + throw new AssertionException(); } return true; } + /** @throws AssertionException */ public static function assertInt(?int $arg): bool { if ($arg === null) { - throw new \Exception(); + throw new AssertionException(); } return true; } /** * @param mixed $arg - * @return bool + * @throws AssertionException */ public function assertNotInt($arg): bool { if (is_int($arg)) { - throw new \Exception(); + throw new AssertionException(); } return true; diff --git a/tests/PHPStan/Tests/AssertionClassMethodTypeSpecifyingExtension.php b/tests/PHPStan/Tests/AssertionClassMethodTypeSpecifyingExtension.php index 203ce65db6..4f8adec652 100644 --- a/tests/PHPStan/Tests/AssertionClassMethodTypeSpecifyingExtension.php +++ b/tests/PHPStan/Tests/AssertionClassMethodTypeSpecifyingExtension.php @@ -13,12 +13,8 @@ class AssertionClassMethodTypeSpecifyingExtension implements MethodTypeSpecifyingExtension { - /** @var bool|null */ - private $nullContext; - - public function __construct(?bool $nullContext) + public function __construct(private ?bool $nullContext = null) { - $this->nullContext = $nullContext; } public function getClass(): string @@ -29,7 +25,7 @@ public function getClass(): string public function isMethodSupported( MethodReflection $methodReflection, MethodCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { if ($this->nullContext === null) { @@ -47,10 +43,10 @@ public function specifyTypes( MethodReflection $methodReflection, MethodCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - return new SpecifiedTypes(['$foo' => [$node->args[0]->value, new StringType()]]); + return new SpecifiedTypes(['$foo' => [$node->getArgs()[0]->value, new StringType()]]); } } diff --git a/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php b/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php index 0961cd6f8f..8a1a20f8c5 100644 --- a/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php +++ b/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php @@ -13,12 +13,8 @@ class AssertionClassStaticMethodTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension { - /** @var bool|null */ - private $nullContext; - - public function __construct(?bool $nullContext) + public function __construct(private ?bool $nullContext = null) { - $this->nullContext = $nullContext; } public function getClass(): string @@ -29,7 +25,7 @@ public function getClass(): string public function isStaticMethodSupported( MethodReflection $staticMethodReflection, StaticCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { if ($this->nullContext === null) { @@ -47,10 +43,10 @@ public function specifyTypes( MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - return new SpecifiedTypes(['$bar' => [$node->args[0]->value, new IntegerType()]]); + return new SpecifiedTypes(['$bar' => [$node->getArgs()[0]->value, new IntegerType()]]); } } diff --git a/tests/PHPStan/Tests/AssertionException.php b/tests/PHPStan/Tests/AssertionException.php new file mode 100644 index 0000000000..0b8f4cbc7c --- /dev/null +++ b/tests/PHPStan/Tests/AssertionException.php @@ -0,0 +1,10 @@ +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 [ @@ -64,19 +75,28 @@ public function dataOr(): array /** * @dataProvider dataOr - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $value - * @param TrinaryLogic ...$operands */ public function testOr( TrinaryLogic $expectedResult, TrinaryLogic $value, - TrinaryLogic ...$operands + TrinaryLogic ...$operands, ): void { $this->assertTrue($expectedResult->equals($value->or(...$operands))); } + /** + * @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 [ @@ -88,8 +108,6 @@ public function dataNegate(): array /** * @dataProvider dataNegate - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $operand */ public function testNegate(TrinaryLogic $expectedResult, TrinaryLogic $operand): void { @@ -137,29 +155,23 @@ public function dataCompareTo(): array /** * @dataProvider dataCompareTo - * @param TrinaryLogic $first - * @param TrinaryLogic $second - * @param TrinaryLogic|null $expected */ public function testCompareTo(TrinaryLogic $first, TrinaryLogic $second, ?TrinaryLogic $expected): void { $this->assertSame( $expected, - $first->compareTo($second) + $first->compareTo($second), ); } /** * @dataProvider dataCompareTo - * @param TrinaryLogic $first - * @param TrinaryLogic $second - * @param TrinaryLogic|null $expected */ public function testCompareToInversed(TrinaryLogic $first, TrinaryLogic $second, ?TrinaryLogic $expected): void { $this->assertSame( $expected, - $second->compareTo($first) + $second->compareTo($first), ); } diff --git a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php index 525b1f6f16..a941dd86ee 100644 --- a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php @@ -2,6 +2,10 @@ namespace PHPStan\Type\Accessory; +use Closure; +use DateTime; +use DateTimeImmutable; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\CallableType; use PHPStan\Type\IntersectionType; @@ -13,8 +17,9 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class HasMethodTypeTest extends \PHPStan\Testing\TestCase +class HasMethodTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -37,7 +42,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasMethodType('format'), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createYes(), ], [ @@ -47,7 +52,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasMethodType('foo'), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createNo(), ], [ @@ -65,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()), @@ -88,15 +88,15 @@ public function dataIsSuperTypeOf(): array [ new HasMethodType('format'), new UnionType([ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\DateTime::class), + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTime::class), ]), TrinaryLogic::createYes(), ], [ new HasMethodType('format'), new UnionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new ObjectType('UnknownClass'), ]), TrinaryLogic::createMaybe(), @@ -104,15 +104,15 @@ public function dataIsSuperTypeOf(): array [ new HasMethodType('format'), new UnionType([ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\Closure::class), + new ObjectType(DateTimeImmutable::class), + new ObjectType(Closure::class), ]), TrinaryLogic::createMaybe(), ], [ new HasMethodType('format'), new IntersectionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IterableType(new MixedType(), new MixedType()), ]), TrinaryLogic::createYes(), @@ -138,9 +138,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -148,7 +145,7 @@ public function testIsSuperTypeOf(HasMethodType $type, Type $otherType, TrinaryL $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -183,7 +180,7 @@ public function dataIsSubTypeOf(): array ], [ new HasMethodType('format'), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createMaybe(), ], ]; @@ -191,9 +188,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -201,15 +195,12 @@ public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLog $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -217,7 +208,7 @@ public function testIsSubTypeOfInversed(HasMethodType $type, Type $otherType, Tr $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php index 9e0012051a..4cecca14c1 100644 --- a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Type\Accessory; +use Closure; +use DateInterval; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\CallableType; use PHPStan\Type\IntersectionType; @@ -13,8 +16,10 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; +use const PHP_VERSION_ID; -class HasPropertyTypeTest extends \PHPStan\Testing\TestCase +class HasPropertyTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -32,7 +37,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasPropertyType('d'), - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), TrinaryLogic::createYes(), ], [ @@ -42,8 +47,8 @@ public function dataIsSuperTypeOf(): array ], [ new HasPropertyType('foo'), - new ObjectType(\Closure::class), - TrinaryLogic::createNo(), + new ObjectType(Closure::class), + PHP_VERSION_ID < 80200 ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(), ], [ new HasPropertyType('foo'), @@ -55,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()), @@ -78,7 +78,7 @@ public function dataIsSuperTypeOf(): array [ new HasPropertyType('d'), new UnionType([ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new ObjectType('UnknownClass'), ]), TrinaryLogic::createMaybe(), @@ -104,9 +104,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -114,7 +111,7 @@ public function testIsSuperTypeOf(HasPropertyType $type, Type $otherType, Trinar $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -144,7 +141,7 @@ public function dataIsSubTypeOf(): array ], [ new HasPropertyType('d'), - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), TrinaryLogic::createMaybe(), ], ]; @@ -152,9 +149,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -162,15 +156,12 @@ public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryL $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -178,7 +169,7 @@ public function testIsSubTypeOfInversed(HasPropertyType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 1dfbec8e19..e701997c6c 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -2,18 +2,20 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +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\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use function array_map; +use function sprintf; -class ArrayTypeTest extends \PHPStan\Testing\TestCase +class ArrayTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -55,14 +57,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], [ - new ArrayType(new MixedType(), new MixedType(false, new UnionType([ - new NullType(), - new ConstantBooleanType(false), - new ConstantIntegerType(0), - new ConstantFloatType(0.0), - new ConstantStringType(''), - new ConstantArrayType([], []), - ]))), + new ArrayType(new MixedType(), new MixedType(false, StaticTypeFactory::falsey())), new ConstantArrayType([], []), TrinaryLogic::createYes(), ], @@ -71,14 +66,27 @@ 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(), + ], ]; } /** * @dataProvider dataIsSuperTypeOf - * @param ArrayType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -86,13 +94,13 @@ public function testIsSuperTypeOf(ArrayType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataAccepts(): array { - $reflectionProvider = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); return [ [ @@ -101,7 +109,7 @@ public function dataAccepts(): array new ConstantArrayType([], []), new ConstantArrayType( [new ConstantIntegerType(0)], - [new MixedType()] + [new MixedType()], ), new ConstantArrayType([ new ConstantIntegerType(0), @@ -109,7 +117,7 @@ public function dataAccepts(): array ], [ new StringType(), new MixedType(), - ]) + ]), ), TrinaryLogic::createYes(), ], @@ -141,21 +149,18 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param ArrayType $acceptingType - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( ArrayType $acceptingType, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } @@ -174,12 +179,10 @@ public function dataDescribe(): array /** * @dataProvider dataDescribe - * @param ArrayType $type - * @param string $expectedDescription */ public function testDescribe( ArrayType $type, - string $expectedDescription + string $expectedDescription, ): void { $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); @@ -187,24 +190,22 @@ public function testDescribe( public function dataInferTemplateTypes(): array { - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'valid templated item' => [ new ArrayType( new MixedType(), - new ObjectType('DateTime') + new ObjectType('DateTime'), ), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'DateTime'], ], @@ -212,7 +213,7 @@ public function dataInferTemplateTypes(): array new MixedType(), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -220,7 +221,7 @@ public function dataInferTemplateTypes(): array new StringType(), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -230,11 +231,11 @@ public function dataInferTemplateTypes(): array new UnionType([ new StringType(), new IntegerType(), - ]) + ]), ), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'int|string'], ], @@ -243,16 +244,16 @@ public function dataInferTemplateTypes(): array new StringType(), new ArrayType( new MixedType(), - new StringType() + new StringType(), ), new ArrayType( new MixedType(), - new IntegerType() + new IntegerType(), ), ]), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'int|string'], ], @@ -269,9 +270,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } diff --git a/tests/PHPStan/Type/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 new file mode 100644 index 0000000000..f7552cba51 --- /dev/null +++ b/tests/PHPStan/Type/BooleanTypeTest.php @@ -0,0 +1,163 @@ +accepts($otherType, true)->result; + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataIsSuperTypeOf(): iterable + { + yield [ + new BooleanType(), + new BooleanType(), + TrinaryLogic::createYes(), + ]; + + yield [ + new BooleanType(), + new ConstantBooleanType(true), + TrinaryLogic::createYes(), + ]; + + yield [ + new BooleanType(), + new MixedType(), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BooleanType(), + new UnionType([new BooleanType(), new StringType()]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new BooleanType(), + new StringType(), + TrinaryLogic::createNo(), + ]; + } + + /** + * @dataProvider dataIsSuperTypeOf + */ + public function testIsSuperTypeOf(BooleanType $type, Type $otherType, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isSuperTypeOf($otherType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataEquals(): array + { + return [ + [ + new BooleanType(), + new BooleanType(), + true, + ], + [ + new ConstantBooleanType(false), + new ConstantBooleanType(false), + true, + ], + [ + new ConstantBooleanType(true), + new ConstantBooleanType(false), + false, + ], + [ + new BooleanType(), + new ConstantBooleanType(false), + false, + ], + [ + new ConstantBooleanType(false), + new BooleanType(), + false, + ], + [ + new BooleanType(), + new IntegerType(), + false, + ], + [ + new ConstantBooleanType(false), + new ConstantIntegerType(0), + false, + ], + ]; + } + + /** + * @dataProvider dataEquals + */ + public function testEquals(BooleanType $type, Type $otherType, bool $expectedResult): void + { + $actualResult = $type->equals($otherType); + $this->assertSame( + $expectedResult, + $actualResult, + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + +} diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 100ddbe415..a59308f40c 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -2,15 +2,24 @@ namespace PHPStan\Type; +use Closure; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\HasMethodType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use function array_map; +use function sprintf; -class CallableTypeTest extends \PHPStan\Testing\TestCase +class CallableTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -41,14 +50,19 @@ 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(), + ], ]; } /** * @dataProvider dataIsSuperTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -56,7 +70,7 @@ public function testIsSuperTypeOf(CallableType $type, Type $otherType, TrinaryLo $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -95,17 +109,12 @@ public function dataIsSubTypeOf(): array ], [ new CallableType(), - new IntersectionType([new CallableType()]), - TrinaryLogic::createYes(), - ], - [ - new CallableType(), - new IntersectionType([new StringType()]), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), TrinaryLogic::createMaybe(), ], [ new CallableType(), - new IntersectionType([new IntegerType()]), + new IntegerType(), TrinaryLogic::createNo(), ], [ @@ -133,9 +142,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -143,15 +149,12 @@ public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -159,31 +162,27 @@ public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, Tri $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } public function dataInferTemplateTypes(): array { - $param = static function (Type $type): NativeParameterReflection { - return new NativeParameterReflection( - '', - false, - $type, - PassedByReference::createNo(), - false, - null - ); - }; + $param = static fn (Type $type): NativeParameterReflection => new NativeParameterReflection( + '', + false, + $type, + PassedByReference::createNo(), + false, + null, + ); - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'template param' => [ @@ -191,13 +190,13 @@ public function dataInferTemplateTypes(): array [ $param(new StringType()), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param($templateType('T')), ], - new IntegerType() + new IntegerType(), ), ['T' => 'string'], ], @@ -206,13 +205,13 @@ public function dataInferTemplateTypes(): array [ $param(new StringType()), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param(new StringType()), ], - $templateType('T') + $templateType('T'), ), ['T' => 'int'], ], @@ -222,16 +221,16 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param(new ObjectType('DateTime')), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), - ['A' => 'DateTime', 'B' => 'int'], + ['B' => 'int', 'A' => 'DateTime'], ], 'receive union' => [ new UnionType([ @@ -241,7 +240,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param(new ObjectType('DateTime')), ], - new IntegerType() + new IntegerType(), ), ]), new CallableType( @@ -249,9 +248,9 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), - ['A' => 'DateTime', 'B' => 'int'], + ['B' => 'int', 'A' => 'DateTime'], ], 'receive non-accepted' => [ new NullType(), @@ -260,7 +259,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), [], ], @@ -277,9 +276,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } @@ -338,25 +335,96 @@ public function dataAccepts(): array TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new CallableType()), TrinaryLogic::createYes(), ], + [ + new CallableType(), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new GenericClassStringType(new ObjectType(Closure::class)), + new ConstantStringType('bind'), + ]), + 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(), + ], ]; } /** * @dataProvider dataAccepts - * @param \PHPStan\Type\CallableType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( CallableType $type, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + $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 1c58e58fcc..f4d19a2760 100644 --- a/tests/PHPStan/Type/ClassStringTypeTest.php +++ b/tests/PHPStan/Type/ClassStringTypeTest.php @@ -2,12 +2,15 @@ namespace PHPStan\Type; -use PHPStan\Testing\TestCase; +use Exception; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use stdClass; +use function sprintf; -class ClassStringTypeTest extends TestCase +class ClassStringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -15,7 +18,7 @@ public function dataIsSuperTypeOf(): array return [ [ new ClassStringType(), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], [ @@ -25,7 +28,7 @@ public function dataIsSuperTypeOf(): array ], [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createYes(), ], [ @@ -45,7 +48,7 @@ public function testIsSuperTypeOf(ClassStringType $type, Type $otherType, Trinar $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -71,7 +74,7 @@ public function dataAccepts(): iterable yield [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createYes(), ]; @@ -83,13 +86,13 @@ public function dataAccepts(): iterable yield [ new ClassStringType(), - new UnionType([new ConstantStringType(\stdClass::class), new ConstantStringType(self::class)]), + new UnionType([new ConstantStringType(stdClass::class), new ConstantStringType(self::class)]), TrinaryLogic::createYes(), ]; yield [ new ClassStringType(), - new UnionType([new ConstantStringType(\stdClass::class), new ConstantStringType('Nonexistent')]), + new UnionType([new ConstantStringType(stdClass::class), new ConstantStringType('Nonexistent')]), TrinaryLogic::createMaybe(), ]; @@ -102,17 +105,43 @@ public function dataAccepts(): iterable /** * @dataProvider dataAccepts - * @param \PHPStan\Type\ClassStringType $type - * @param Type $otherType - * @param \PHPStan\TrinaryLogic $expectedResult */ public function testAccepts(ClassStringType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataEquals(): array + { + return [ + [ + new ClassStringType(), + new ClassStringType(), + true, + ], + [ + new ClassStringType(), + new StringType(), + false, + ], + ]; + } + + /** + * @dataProvider dataEquals + */ + public function testEquals(ClassStringType $type, Type $otherType, bool $expectedResult): void + { + $actualResult = $type->equals($otherType); + $this->assertSame( + $expectedResult, + $actualResult, + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } 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 c788121178..ccaaa4ca78 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use Closure; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use function sprintf; -class ClosureTypeTest extends \PHPStan\Testing\TestCase +class ClosureTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -12,7 +15,7 @@ public function dataIsSuperTypeOf(): array return [ [ new ClosureType([], new MixedType(), false), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createMaybe(), ], [ @@ -20,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), @@ -31,7 +39,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], @@ -71,13 +79,13 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createNo(), ], [ - new ObjectWithoutClassType(new ObjectType(\Closure::class)), + new ObjectWithoutClassType(new ObjectType(Closure::class)), new ClosureType([], new MixedType(), false), TrinaryLogic::createNo(), ], [ new ClosureType([], new MixedType(), false), - new ObjectWithoutClassType(new ObjectType(\Closure::class)), + new ObjectWithoutClassType(new ObjectType(Closure::class)), TrinaryLogic::createNo(), ], ]; @@ -85,21 +93,18 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf( Type $type, Type $otherType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $actualResult = $type->isSuperTypeOf($otherType); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Constant/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 bd4199a988..b047b86a69 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -2,21 +2,33 @@ namespace PHPStan\Type\Constant; +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; use PHPStan\Type\Generic\TemplateTypeFactory; 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; -class ConstantArrayTypeTest extends \PHPStan\Testing\TestCase +class ConstantArrayTypeTest extends PHPStanTestCase { public function dataAccepts(): iterable @@ -124,7 +136,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ]) + ]), ), new ConstantArrayType([ new ConstantStringType('name'), @@ -161,7 +173,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ]) + ]), ), new ConstantArrayType([ new ConstantStringType('surname'), @@ -178,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'), @@ -214,7 +226,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new IntegerType(), - ], 0, [1]), + ], [0], [1]), new ConstantArrayType([ new ConstantStringType('sorton'), new ConstantStringType('limit'), @@ -230,7 +242,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0, [0]), + ], [0], [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -244,7 +256,7 @@ public function dataAccepts(): iterable new ConstantStringType('limit'), ], [ new IntegerType(), - ], 0), + ], [0]), new ConstantArrayType([ new ConstantStringType('limit'), ], [ @@ -260,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'), @@ -278,7 +290,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('color'), ], [ @@ -294,7 +306,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ], 0, [0, 1]), + ], [0], [0, 1]), new ConstantArrayType([ new ConstantStringType('sound'), ], [ @@ -310,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(), ]; @@ -328,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'), @@ -338,21 +350,75 @@ 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(), + ]; } /** * @dataProvider dataAccepts - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ 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(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -449,13 +515,186 @@ public function dataIsSuperTypeOf(): iterable ]), TrinaryLogic::createNo(), ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [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(), + ]; } /** * @dataProvider dataIsSuperTypeOf - * @param ConstantArrayType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -463,20 +702,18 @@ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, Trin $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataInferTemplateTypes(): array { - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'receive constant array' => [ @@ -488,7 +725,7 @@ public function dataInferTemplateTypes(): array [ new StringType(), new IntegerType(), - ] + ], ), new ConstantArrayType( [ @@ -498,7 +735,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), ['T' => 'string', 'U' => 'int'], ], @@ -511,7 +748,7 @@ public function dataInferTemplateTypes(): array [ new StringType(), new IntegerType(), - ] + ], ), new ConstantArrayType( [ @@ -521,7 +758,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), ['T' => 'string', 'U' => 'int'], ], @@ -532,7 +769,7 @@ public function dataInferTemplateTypes(): array ], [ new StringType(), - ] + ], ), new ConstantArrayType( [ @@ -542,7 +779,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), [], ], @@ -554,7 +791,7 @@ public function dataInferTemplateTypes(): array ], [ $templateType('T'), - ] + ], ), [], ], @@ -566,7 +803,7 @@ public function dataInferTemplateTypes(): array ], [ $templateType('T'), - ] + ], ), ['T' => 'string'], ], @@ -583,9 +820,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } @@ -598,7 +833,7 @@ public function testIsCallable(ConstantArrayType $type, TrinaryLogic $expectedRe $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } @@ -623,7 +858,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), @@ -634,7 +869,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('foobar'), ]), TrinaryLogic::createNo(), @@ -656,11 +891,164 @@ public function dataIsCallable(): iterable new ConstantStringType('a'), new ConstantStringType('b'), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('bind'), ]), TrinaryLogic::createNo(), ]; + + yield 'class-string' => [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new GenericClassStringType(new ObjectType(Closure::class)), + new ConstantStringType('bind'), + ]), + TrinaryLogic::createYes(), + ]; + } + + 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 b8261bbea7..2122e3c829 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Type\Constant; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; -class ConstantFloatTypeTest extends \PHPStan\Testing\TestCase +class ConstantFloatTypeTest extends PHPStanTestCase { public function dataDescribe(): array @@ -22,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', @@ -31,12 +40,10 @@ public function dataDescribe(): array /** * @dataProvider dataDescribe - * @param ConstantFloatType $type - * @param string $expectedDescription */ public function testDescribe( ConstantFloatType $type, - string $expectedDescription + string $expectedDescription, ): void { $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php index 5a23289569..c33c33e5a6 100644 --- a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Type\Constant; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class ConstantIntegerTypeTest extends \PHPStan\Testing\TestCase +class ConstantIntegerTypeTest extends PHPStanTestCase { public function dataAccepts(): iterable @@ -33,17 +35,14 @@ public function dataAccepts(): iterable /** * @dataProvider dataAccepts - * @param ConstantIntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(ConstantIntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -70,9 +69,6 @@ public function dataIsSuperTypeOf(): iterable /** * @dataProvider dataIsSuperTypeOf - * @param ConstantIntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ConstantIntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -80,7 +76,7 @@ public function testIsSuperTypeOf(ConstantIntegerType $type, Type $otherType, Tr $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index dfe9fb2425..2098a1f02b 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -2,126 +2,135 @@ namespace PHPStan\Type\Constant; -use PHPStan\Testing\TestCase; +use Exception; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\ErrorType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use stdClass; +use Throwable; +use function sprintf; -class ConstantStringTypeTest extends TestCase +class ConstantStringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array { + $reflectionProvider = $this->createReflectionProvider(); return [ 0 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createMaybe(), ], 1 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), TrinaryLogic::createMaybe(), ], 2 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createNo(), ], 3 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createNo(), ], 4 => [ - new ConstantStringType(\Exception::class), - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 5 => [ - new ConstantStringType(\Exception::class), - new ConstantStringType(\InvalidArgumentException::class), + new ConstantStringType(Exception::class), + new ConstantStringType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], 6 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createMaybe(), ], 7 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createNo(), ], 8 => [ - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], 9 => [ - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], 10 => [ - new ConstantStringType(\InvalidArgumentException::class), + new ConstantStringType(InvalidArgumentException::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], 11 => [ - new ConstantStringType(\Throwable::class), + new ConstantStringType(Throwable::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createNo(), ], 12 => [ - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createNo(), ], 13 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new StaticType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), TrinaryLogic::createMaybe(), ], 14 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new StaticType(\InvalidArgumentException::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(InvalidArgumentException::class))), TrinaryLogic::createNo(), ], 15 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new StaticType(\Throwable::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Throwable::class))), TrinaryLogic::createMaybe(), ], ]; @@ -136,16 +145,45 @@ public function testIsSuperTypeOf(ConstantStringType $type, Type $otherType, Tri $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function testGeneralize(): void { - $this->assertSame('string', (new ConstantStringType('NonexistentClass'))->generalize()->describe(VerbosityLevel::precise())); - $this->assertSame('string', (new ConstantStringType(\stdClass::class))->generalize()->describe(VerbosityLevel::precise())); - $this->assertSame('class-string', (new ConstantStringType(\stdClass::class, true))->generalize()->describe(VerbosityLevel::precise())); - $this->assertSame('class-string', (new ConstantStringType('NonexistentClass', true))->generalize()->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-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&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-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())); + } + + public function testTextInvalidEncoding(): void + { + $this->assertSame("'\xc3Lorem ipsum dolor s\u{2026}'", (new ConstantStringType("\xc3Lorem ipsum dolor sit"))->describe(VerbosityLevel::value())); + } + + public function testShortTextInvalidEncoding(): void + { + $this->assertSame("'\xc3Lorem ipsum dolor'", (new ConstantStringType("\xc3Lorem ipsum dolor"))->describe(VerbosityLevel::value())); + } + + public function testSetInvalidValue(): void + { + $string = new ConstantStringType('internal:/node/add'); + $result = $string->setOffsetValueType(new ConstantIntegerType(0), new NullType()); + $this->assertInstanceOf(ErrorType::class, $result); } } diff --git a/tests/PHPStan/Type/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 new file mode 100644 index 0000000000..1bf9013c42 --- /dev/null +++ b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php @@ -0,0 +1,227 @@ +markTestSkipped('Test requires PHP 8.1.'); + } + $actualResult = $type->isSuperTypeOf($otherType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataAccepts(): iterable + { + yield [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + TrinaryLogic::createYes(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + TrinaryLogic::createYes(), + ]; + + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('PHPStan\Fixture\TestEnum'), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('stdClass'), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType(FinalClass::class), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('Stringable'), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(new ObjectType('PHPStan\Fixture\TestEnum')), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(new ObjectType('PHPStan\Fixture\TestEnumInterface')), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('PHPStan\Fixture\TestEnumInterface', new ObjectType('PHPStan\Fixture\TestEnum')), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(new ObjectType('PHPStan\Fixture\AnotherTestEnum')), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'THREE'), + ]), + TrinaryLogic::createNo(), + ]; + } + + /** + * @dataProvider dataAccepts + */ + public function testAccepts( + Type $type, + Type $acceptedType, + TrinaryLogic $expectedResult, + ): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->assertSame( + $expectedResult->describe(), + $type->accepts($acceptedType, true)->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 454489c3db..c1af0c7740 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -2,7 +2,14 @@ namespace PHPStan\Type; -class FileTypeMapperTest extends \PHPStan\Testing\TestCase +use DependentPhpDocs\Foo; +use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; +use RuntimeException; +use function realpath; + +class FileTypeMapperTest extends PHPStanTestCase { public function testGetResolvedPhpDoc(): void @@ -10,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 * @@ -26,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 @@ -52,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())); @@ -72,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()); @@ -85,21 +98,21 @@ public function testFileWithDependentPhpDocs(): void $realpath = realpath(__DIR__ . '/data/dependent-phpdocs.php'); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc( $realpath, - \DependentPhpDocs\Foo::class, + Foo::class, null, 'addPages', - '/** @param Foo[]|Foo|\Iterator $pages */' + '/** @param Foo[]|Foo|\Iterator $pages */', ); $this->assertCount(1, $resolved->getParamTags()); $this->assertSame( '(DependentPhpDocs\Foo&iterable)|(iterable&Iterator)', - $resolved->getParamTags()['pages']->getType()->describe(VerbosityLevel::precise()) + $resolved->getParamTags()['pages']->getType()->describe(VerbosityLevel::precise()), ); } @@ -110,7 +123,7 @@ public function testFileThrowsPhpDocs(): void $realpath = realpath(__DIR__ . '/data/throws-phpdocs.php'); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc($realpath, \ThrowsPhpDocs\Foo::class, null, 'throwRuntimeException', '/** @@ -119,8 +132,8 @@ public function testFileThrowsPhpDocs(): void $this->assertNotNull($resolved->getThrowsTag()); $this->assertSame( - \RuntimeException::class, - $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()) + RuntimeException::class, + $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()), ); $resolved = $fileTypeMapper->getResolvedPhpDoc($realpath, \ThrowsPhpDocs\Foo::class, null, 'throwRuntimeAndLogicException', '/** @@ -130,7 +143,7 @@ public function testFileThrowsPhpDocs(): void $this->assertNotNull($resolved->getThrowsTag()); $this->assertSame( 'LogicException|RuntimeException', - $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()) + $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()), ); $resolved = $fileTypeMapper->getResolvedPhpDoc($realpath, \ThrowsPhpDocs\Foo::class, null, 'throwRuntimeAndLogicException2', '/** @@ -141,20 +154,20 @@ public function testFileThrowsPhpDocs(): void $this->assertNotNull($resolved->getThrowsTag()); $this->assertSame( 'LogicException|RuntimeException', - $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()) + $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()), ); } public function testFileWithCyclicPhpDocs(): void { - self::getContainer()->getByType(\PHPStan\Broker\Broker::class); + $this->createReflectionProvider(); /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); $realpath = realpath(__DIR__ . '/data/cyclic-phpdocs.php'); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc( @@ -162,12 +175,24 @@ public function testFileWithCyclicPhpDocs(): void \CyclicPhpDocs\Foo::class, null, 'getIterator', - '/** @return iterable | Foo */' + '/** @return iterable | Foo */', ); - /** @var \PHPStan\PhpDoc\Tag\ReturnTag $returnTag */ + /** @var ReturnTag $returnTag */ $returnTag = $resolved->getReturnTag(); $this->assertSame('CyclicPhpDocs\Foo|iterable', $returnTag->getType()->describe(VerbosityLevel::precise())); } + public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void + { + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); + + $doc1 = $fileTypeMapper->getResolvedPhpDoc(__DIR__ . '/data/alias-collision1.php', null, null, null, '/** @var Foo $x */'); + $doc2 = $fileTypeMapper->getResolvedPhpDoc(__DIR__ . '/data/alias-collision2.php', null, null, null, '/** @var Foo $x */'); + + $this->assertSame('AliasCollisionNamespace1\Foo', $doc1->getVarTags()['x']->getType()->describe(VerbosityLevel::precise())); + $this->assertSame('AliasCollisionNamespace2\Foo', $doc2->getVarTags()['x']->getType()->describe(VerbosityLevel::precise())); + } + } diff --git a/tests/PHPStan/Type/FloatTypeTest.php b/tests/PHPStan/Type/FloatTypeTest.php index 5a0362c7a8..04a9c270ca 100644 --- a/tests/PHPStan/Type/FloatTypeTest.php +++ b/tests/PHPStan/Type/FloatTypeTest.php @@ -2,9 +2,14 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use function sprintf; -class FloatTypeTest extends \PHPStan\Testing\TestCase +class FloatTypeTest extends PHPStanTestCase { public function dataAccepts(): array @@ -57,17 +62,74 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ 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(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataEquals(): array + { + return [ + [ + new FloatType(), + new FloatType(), + true, + ], + [ + new ConstantFloatType(0.0), + new ConstantFloatType(0.0), + true, + ], + [ + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + false, + ], + [ + new FloatType(), + new ConstantFloatType(0.0), + false, + ], + [ + new ConstantFloatType(0.0), + new FloatType(), + false, + ], + [ + new FloatType(), + new IntegerType(), + false, + ], + [ + new ConstantFloatType(0.0), + new ConstantIntegerType(0), + false, + ], + [ + new ConstantFloatType(0.0), + new ConstantStringType('0.0'), + false, + ], + ]; + } + + /** + * @dataProvider dataEquals + */ + public function testEquals(FloatType $type, Type $otherType, bool $expectedResult): void + { + $actualResult = $type->equals($otherType); + $this->assertSame( + $expectedResult, + $actualResult, + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php index a34fb01fdf..ef6a0faf8d 100644 --- a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php @@ -2,9 +2,17 @@ namespace PHPStan\Type\Generic; +use DateTime; +use Exception; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClassStringType; +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; use PHPStan\Type\StaticType; @@ -12,61 +20,66 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use stdClass; +use Throwable; +use function sprintf; -class GenericClassStringTypeTest extends \PHPStan\Testing\TestCase +class GenericClassStringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ 0 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new ClassStringType(), TrinaryLogic::createMaybe(), ], 1 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), TrinaryLogic::createMaybe(), ], 2 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], 3 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), TrinaryLogic::createMaybe(), ], 4 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 5 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createNo(), ], 6 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 7 => [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 8 => [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), - TrinaryLogic::createNo(), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), + TrinaryLogic::createMaybe(), ], 9 => [ - new GenericClassStringType(new ObjectType(\stdClass::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(stdClass::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createNo(), ], 10 => [ @@ -74,66 +87,74 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 11 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 12 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createNo(), ], 13 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\InvalidArgumentException::class), + new ConstantStringType(InvalidArgumentException::class), TrinaryLogic::createYes(), ], 14 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\Throwable::class), - TrinaryLogic::createNo(), + new ConstantStringType(Throwable::class), + TrinaryLogic::createMaybe(), ], 15 => [ - new GenericClassStringType(new StaticType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 16 => [ - new GenericClassStringType(new StaticType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), - TrinaryLogic::createNo(), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(InvalidArgumentException::class))), + new ConstantStringType(Exception::class), + TrinaryLogic::createMaybe(), ], 17 => [ - new GenericClassStringType(new StaticType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Throwable::class))), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], + 18 => [ + new GenericClassStringType(new ObjectType(Type::class, new UnionType([ + new ObjectType(ConstantIntegerType::class), + new ObjectType(IntegerRangeType::class), + ]))), + new ConstantStringType(IntegerType::class), + TrinaryLogic::createMaybe(), + ], ]; } @@ -146,7 +167,7 @@ public function testIsSuperTypeOf(GenericClassStringType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -154,33 +175,33 @@ public function dataAccepts(): array { return [ 0 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Throwable::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Throwable::class), TrinaryLogic::createNo(), ], 1 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 2 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\InvalidArgumentException::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(InvalidArgumentException::class), TrinaryLogic::createYes(), ], 3 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), TrinaryLogic::createMaybe(), ], 4 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ObjectType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ObjectType(Exception::class), TrinaryLogic::createNo(), ], 5 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], 6 => [ @@ -188,7 +209,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new ConstantStringType('NonexistentClass'), TrinaryLogic::createNo(), @@ -198,11 +219,11 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new UnionType([ - new ConstantStringType(\DateTime::class), - new ConstantStringType(\Exception::class), + new ConstantStringType(DateTime::class), + new ConstantStringType(Exception::class), ]), TrinaryLogic::createYes(), ], @@ -211,7 +232,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new ClassStringType(), TrinaryLogic::createYes(), @@ -221,14 +242,34 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithClass('Boo'), 'U', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), + )), + TrinaryLogic::createMaybe(), + ], + 10 => [ + new GenericClassStringType(TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + )), + new UnionType([new IntegerType(), new StringType()]), + TrinaryLogic::createMaybe(), + ], + 11 => [ + new GenericClassStringType(TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), )), + new BenevolentUnionType([new IntegerType(), new StringType()]), TrinaryLogic::createMaybe(), ], ]; @@ -240,14 +281,66 @@ public function dataAccepts(): array public function testAccepts( GenericClassStringType $acceptingType, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), + ); + } + + public function dataEquals(): array + { + $reflectionProvider = $this->createReflectionProvider(); + + return [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + true, + ], + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), + false, + ], + [ + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + true, + ], + [ + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(stdClass::class))), + false, + ], + ]; + } + + /** + * @dataProvider dataEquals + */ + public function testEquals(GenericClassStringType $type, Type $otherType, bool $expected): void + { + $verbosityLevel = VerbosityLevel::precise(); + $typeDescription = $type->describe($verbosityLevel); + $otherTypeDescription = $otherType->describe($verbosityLevel); + + $actual = $type->equals($otherType); + $this->assertSame( + $expected, + $actual, + sprintf('%s -> equals(%s)', $typeDescription, $otherTypeDescription), + ); + + $actual = $otherType->equals($type); + $this->assertSame( + $expected, + $actual, + sprintf('%s -> equals(%s)', $otherTypeDescription, $typeDescription), ); } diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index a8a4672112..3be9b7193d 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -2,20 +2,33 @@ namespace PHPStan\Type\Generic; +use DateTime; +use DateTimeInterface; +use Exception; +use Iterator; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Test\A; 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; +use ReflectionClass; +use stdClass; +use Traversable; +use function array_map; +use function sprintf; +use const PHP_VERSION_ID; -class GenericObjectTypeTest extends \PHPStan\Testing\TestCase +class GenericObjectTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -52,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(), ], @@ -84,13 +97,173 @@ public function dataIsSuperTypeOf(): array 'covariant with super type' => [ new GenericObjectType(C\Covariant::class, [new ObjectType('DateTime')]), 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, [ + new ObjectType(stdClass::class), + ]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), + ]), + new ObjectType(ReflectionClass::class), + TrinaryLogic::createMaybe(), + ], + [ + new GenericObjectType(ReflectionClass::class, [ + new ObjectWithoutClassType(), + ]), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), + ]), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), + ]), + new GenericObjectType(ReflectionClass::class, [ + new ObjectWithoutClassType(), + ]), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(), + ], + [ + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(Exception::class), + ]), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), + ]), + 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 { @@ -98,7 +271,7 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -136,23 +309,23 @@ 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 ObjectType(\Traversable::class), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), + new ObjectType(Traversable::class), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\Iterator::class, [new MixedType(true), new MixedType(true)]), - new ObjectType(\Iterator::class), + new GenericObjectType(Iterator::class, [new MixedType(true), new MixedType(true)]), + new ObjectType(Iterator::class), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\Iterator::class, [new MixedType(true), new MixedType(true)]), - new IntersectionType([new ObjectType(\Iterator::class), new ObjectType(\DateTimeInterface::class)]), + new GenericObjectType(Iterator::class, [new MixedType(true), new MixedType(true)]), + new IntersectionType([new ObjectType(Iterator::class), new ObjectType(DateTimeInterface::class)]), TrinaryLogic::createYes(), ], ]; @@ -160,37 +333,36 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts + * @dataProvider dataTypeProjections */ public function testAccepts( Type $acceptingType, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { - $actualResult = $acceptingType->accepts($acceptedType, true); + $actualResult = $acceptingType->accepts($acceptedType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static function (string $name, ?Type $bound = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $bound ?? new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $bound ?? new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'simple' => [ new GenericObjectType(A\A::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), new GenericObjectType(A\A::class, [ $templateType('T'), @@ -199,7 +371,7 @@ public function dataInferTemplateTypes(): array ], 'two types' => [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), new GenericObjectType(A\A2::class, [ @@ -211,12 +383,12 @@ public function dataInferTemplateTypes(): array 'union' => [ new UnionType([ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), new GenericObjectType(A\A2::class, [ new IntegerType(), - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ]), new GenericObjectType(A\A2::class, [ @@ -228,7 +400,7 @@ public function dataInferTemplateTypes(): array 'nested' => [ new GenericObjectType(A\A::class, [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), ]), @@ -242,27 +414,27 @@ public function dataInferTemplateTypes(): array ], 'missing type' => [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), new GenericObjectType(A\A2::class, [ - $templateType('K', new ObjectType(\DateTimeInterface::class)), - $templateType('V', new ObjectType(\DateTimeInterface::class)), + $templateType('K', new ObjectType(DateTimeInterface::class)), + $templateType('V', new ObjectType(DateTimeInterface::class)), ]), ['K' => 'DateTime'], ], 'wrong class' => [ new GenericObjectType(B\I::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), new GenericObjectType(A\A::class, [ - $templateType('T', new ObjectType(\DateTimeInterface::class)), + $templateType('T', new ObjectType(DateTimeInterface::class)), ]), [], ], 'wrong type' => [ new IntegerType(), new GenericObjectType(A\A::class, [ - $templateType('T', new ObjectType(\DateTimeInterface::class)), + $templateType('T', new ObjectType(DateTimeInterface::class)), ]), [], ], @@ -286,27 +458,19 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } /** @return array}> */ public function dataGetReferencedTypeArguments(): array { - $templateType = static function (string $name, ?Type $bound = null): TemplateType { - $templateType = TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $bound ?? new MixedType(), - TemplateTypeVariance::createInvariant() - ); - if (!$templateType instanceof TemplateType) { - throw new \PHPStan\ShouldNotHappenException(); - } - return $templateType; - }; + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $bound ?? new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'param: Invariant' => [ @@ -317,7 +481,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -329,7 +493,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -343,7 +507,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -359,7 +523,77 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), + ), + ], + ], + '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(), ), ], ], @@ -371,7 +605,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -383,7 +617,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -397,7 +631,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -413,7 +647,193 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), + ), + ], + ], + '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(), ), ], ], @@ -427,7 +847,109 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), + ), + ], + ], + '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(), ), ], ], @@ -446,19 +968,15 @@ public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVar $result[] = $r; } - $comparableResult = array_map(static function (TemplateTypeReference $ref): array { - return [ - 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), - 'positionVariance' => $ref->getPositionVariance()->describe(), - ]; - }, $result); + $comparableResult = array_map(static fn (TemplateTypeReference $ref): array => [ + 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), + 'positionVariance' => $ref->getPositionVariance()->describe(), + ], $result); - $comparableExpect = array_map(static function (TemplateTypeReference $ref): array { - return [ - 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), - 'positionVariance' => $ref->getPositionVariance()->describe(), - ]; - }, $expectedReferences); + $comparableExpect = array_map(static fn (TemplateTypeReference $ref): array => [ + 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), + 'positionVariance' => $ref->getPositionVariance()->describe(), + ], $expectedReferences); $this->assertSame($comparableExpect, $comparableResult); } diff --git a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php index 279617324e..2bcde9560a 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php @@ -2,11 +2,13 @@ namespace PHPStan\Type\Generic; +use DateTime; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; -class TemplateTypeHelperTest extends \PHPStan\Testing\TestCase +class TemplateTypeHelperTest extends PHPStanTestCase { public function testIssue2512(): void @@ -15,34 +17,38 @@ public function testIssue2512(): void TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); $type = TemplateTypeHelper::resolveTemplateTypes( $templateType, new TemplateTypeMap([ 'T' => $templateType, - ]) + ]), + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), ); $this->assertEquals( 'T (function a(), parameter)', - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); $type = TemplateTypeHelper::resolveTemplateTypes( $templateType, new TemplateTypeMap([ 'T' => new IntersectionType([ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), $templateType, ]), - ]) + ]), + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), ); $this->assertEquals( 'DateTime&T (function a(), parameter)', - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); } diff --git a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php new file mode 100644 index 0000000000..ff9f5fe58e --- /dev/null +++ b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php @@ -0,0 +1,67 @@ + new ObjectType(Exception::class), + ]))->convertToLowerBoundTypes(); + + yield [ + $map, + Exception::class, + ]; + + yield [ + $map->union(new TemplateTypeMap([ + 'T' => new ObjectType(InvalidArgumentException::class), + ])), + InvalidArgumentException::class, + ]; + + yield [ + $map->union((new TemplateTypeMap([ + 'T' => new ObjectType(InvalidArgumentException::class), + ]))->convertToLowerBoundTypes()), + InvalidArgumentException::class, + ]; + + yield [ + (new TemplateTypeMap([ + 'T' => new ObjectType(Exception::class), + ], [ + 'T' => new ObjectType(InvalidArgumentException::class), + ]))->convertToLowerBoundTypes(), + InvalidArgumentException::class, + ]; + + yield [ + (new TemplateTypeMap([ + 'T' => new ObjectType(InvalidArgumentException::class), + ], [ + 'T' => new ObjectType(Exception::class), + ]))->convertToLowerBoundTypes(), + InvalidArgumentException::class, + ]; + } + + /** @dataProvider dataUnionWithLowerBoundTypes */ + public function testUnionWithLowerBoundTypes(TemplateTypeMap $map, string $expectedTDescription): void + { + $this->assertFalse($map->isEmpty()); + $t = $map->getType('T'); + $this->assertNotNull($t); + $this->assertSame($expectedTDescription, $t->describe(VerbosityLevel::precise())); + } + +} diff --git a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php new file mode 100644 index 0000000000..c5bd9ac0a6 --- /dev/null +++ b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php @@ -0,0 +1,103 @@ +assertSame( + $expected->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($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 @@ +assertTrue($integerType->accepts(new IntegerType(), true)->yes()); - $this->assertTrue($integerType->accepts(new ConstantIntegerType(1), true)->yes()); - $this->assertTrue($integerType->accepts(new NullType(), true)->no()); - $this->assertTrue($integerType->accepts(new MixedType(), true)->yes()); - $this->assertTrue($integerType->accepts(new FloatType(), true)->no()); - $this->assertTrue($integerType->accepts(new StringType(), true)->no()); + /** + * @dataProvider dataAccepts + */ + public function testAccepts(IntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void + { + $actualResult = $type->accepts($otherType, true)->result; + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); } public function dataIsSuperTypeOf(): iterable @@ -55,9 +96,6 @@ public function dataIsSuperTypeOf(): iterable /** * @dataProvider dataIsSuperTypeOf - * @param IntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(IntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -65,7 +103,66 @@ public function testIsSuperTypeOf(IntegerType $type, Type $otherType, TrinaryLog $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataEquals(): array + { + return [ + [ + new IntegerType(), + new IntegerType(), + true, + ], + [ + new ConstantIntegerType(0), + new ConstantIntegerType(0), + true, + ], + [ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + false, + ], + [ + new IntegerType(), + new ConstantIntegerType(0), + false, + ], + [ + new ConstantIntegerType(0), + new IntegerType(), + false, + ], + [ + new IntegerType(), + new FloatType(), + false, + ], + [ + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + false, + ], + [ + new ConstantIntegerType(0), + new ConstantStringType('0'), + false, + ], + ]; + } + + /** + * @dataProvider dataEquals + */ + public function testEquals(IntegerType $type, Type $otherType, bool $expectedResult): void + { + $actualResult = $type->equals($otherType); + $this->assertSame( + $expectedResult, + $actualResult, + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 634c11b0b4..d5259d53e5 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -2,19 +2,32 @@ namespace PHPStan\Type; +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 \PHPStan\Testing\TestCase +class IntersectionTypeTest extends PHPStanTestCase { - public function dataAccepts(): \Iterator + public function dataAccepts(): Iterator { $intersectionType = new IntersectionType([ new ObjectType('Collection'), @@ -36,7 +49,7 @@ public function dataAccepts(): \Iterator yield [ $intersectionType, new IterableType(new MixedType(), new ObjectType('Item')), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; yield [ @@ -51,23 +64,20 @@ public function dataAccepts(): \Iterator yield [ TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new CallableType()), new CallableType(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; } /** * @dataProvider dataAccepts - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ 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(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -78,7 +88,7 @@ public function dataIsCallable(): array new IntersectionType([ new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], - [new ConstantStringType('Closure'), new ConstantStringType('bind')] + [new ConstantStringType('Closure'), new ConstantStringType('bind')], ), new IterableType(new MixedType(), new ObjectType('Item')), ]), @@ -103,8 +113,6 @@ public function dataIsCallable(): array /** * @dataProvider dataIsCallable - * @param IntersectionType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedResult): void { @@ -112,11 +120,11 @@ public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedRes $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsSuperTypeOf(): \Iterator + public function dataIsSuperTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -147,82 +155,26 @@ public function dataIsSuperTypeOf(): \Iterator TrinaryLogic::createNo(), ]; - $intersectionTypeB = new IntersectionType([ - new IntegerType(), - ]); - - yield [ - $intersectionTypeB, - $intersectionTypeB, - TrinaryLogic::createYes(), - ]; - - yield [ - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - 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), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; @@ -230,57 +182,60 @@ public function dataIsSuperTypeOf(): \Iterator yield [ new IntersectionType([ new ObjectType(\TestIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ new ObjectType(\TestIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; + + yield [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), + TrinaryLogic::createMaybe(), + ]; } /** * @dataProvider dataIsSuperTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -288,11 +243,11 @@ public function testIsSuperTypeOf(IntersectionType $type, Type $otherType, Trina $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): \Iterator + public function dataIsSubTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -335,16 +290,6 @@ public function dataIsSubTypeOf(): \Iterator TrinaryLogic::createNo(), ]; - $intersectionTypeB = new IntersectionType([ - new IntegerType(), - ]); - - yield [ - $intersectionTypeB, - $intersectionTypeB, - TrinaryLogic::createYes(), - ]; - $intersectionTypeC = new IntersectionType([ new StringType(), new CallableType(), @@ -391,9 +336,6 @@ public function dataIsSubTypeOf(): \Iterator /** * @dataProvider dataIsSubTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -401,15 +343,12 @@ public function testIsSubTypeOf(IntersectionType $type, Type $otherType, Trinary $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -417,14 +356,379 @@ public function testIsSubTypeOfInversed(IntersectionType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } public function testToBooleanCrash(): void { $type = new IntersectionType([new NeverType(), new NonEmptyArrayType()]); - $this->assertSame('bool', $type->toBoolean()->describe(VerbosityLevel::precise())); + $this->assertSame('true', $type->toBoolean()->describe(VerbosityLevel::precise())); + } + + 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 7aaf86149c..013c0fd15c 100644 --- a/tests/PHPStan/Type/IterableTypeTest.php +++ b/tests/PHPStan/Type/IterableTypeTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasPropertyType; @@ -10,8 +11,10 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use function array_map; +use function sprintf; -class IterableTypeTest extends \PHPStan\Testing\TestCase +class IterableTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -57,9 +60,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -67,7 +67,7 @@ public function testIsSuperTypeOf(IterableType $type, Type $otherType, TrinaryLo $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -159,9 +159,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -169,15 +166,12 @@ public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -185,41 +179,39 @@ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, Tri $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } public function dataInferTemplateTypes(): array { - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'receive iterable' => [ new IterableType( new MixedType(), - new ObjectType('DateTime') + new ObjectType('DateTime'), ), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'DateTime'], ], 'receive iterable template key' => [ new IterableType( new StringType(), - new ObjectType('DateTime') + new ObjectType('DateTime'), ), new IterableType( $templateType('U'), - $templateType('T') + $templateType('T'), ), ['U' => 'string', 'T' => 'DateTime'], ], @@ -227,7 +219,7 @@ public function dataInferTemplateTypes(): array new MixedType(), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -235,7 +227,7 @@ public function dataInferTemplateTypes(): array new StringType(), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -252,9 +244,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } @@ -264,7 +254,7 @@ public function dataDescribe(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); return [ @@ -312,13 +302,13 @@ public function dataAccepts(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); return [ [ new IterableType( new MixedType(), - $t + $t, ), new ConstantArrayType([], []), TrinaryLogic::createYes(), @@ -326,7 +316,7 @@ public function dataAccepts(): array [ new IterableType( new MixedType(), - $t->toArgument() + $t->toArgument(), ), new ConstantArrayType([], []), TrinaryLogic::createYes(), @@ -336,17 +326,14 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param IterableType $iterableType - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(IterableType $iterableType, Type $otherType, TrinaryLogic $expectedResult): void { - $actualResult = $iterableType->accepts($otherType, true); + $actualResult = $iterableType->accepts($otherType, true)->result; $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $iterableType->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $iterableType->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index acd24ecad6..3ffc8f9db7 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -2,10 +2,20 @@ 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 \PHPStan\Testing\TestCase +class MixedTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -141,14 +151,16 @@ public function dataIsSuperTypeOf(): array new UnionType([new StringType(), new IntegerType()]), TrinaryLogic::createYes(), ], + 26 => [ + new MixedType(), + new StrictMixedType(), + TrinaryLogic::createYes(), + ], ]; } /** * @dataProvider dataIsSuperTypeOf - * @param \PHPStan\Type\MixedType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -156,7 +168,999 @@ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + 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 d802f2907f..9a1bdf2fea 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -2,16 +2,49 @@ namespace PHPStan\Type; +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 \PHPStan\Testing\TestCase +class ObjectTypeTest extends PHPStanTestCase { public function dataIsIterable(): array @@ -26,8 +59,6 @@ public function dataIsIterable(): array /** * @dataProvider dataIsIterable - * @param ObjectType $type - * @param TrinaryLogic $expectedResult */ public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): void { @@ -35,7 +66,36 @@ public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), + ); + } + + /** + * @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())), ); } @@ -50,8 +110,6 @@ public function dataIsCallable(): array /** * @dataProvider dataIsCallable - * @param ObjectType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): void { @@ -59,12 +117,14 @@ public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } public function dataIsSuperTypeOf(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ 0 => [ new ObjectType('UnknownClassA'), @@ -72,164 +132,164 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], 1 => [ - new ObjectType(\ArrayAccess::class), - new ObjectType(\Traversable::class), + new ObjectType(ArrayAccess::class), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 2 => [ - new ObjectType(\Countable::class), - new ObjectType(\Countable::class), + new ObjectType(Countable::class), + new ObjectType(Countable::class), TrinaryLogic::createYes(), ], 3 => [ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createYes(), ], 4 => [ - new ObjectType(\Traversable::class), - new ObjectType(\ArrayObject::class), + new ObjectType(Traversable::class), + new ObjectType(ArrayObject::class), TrinaryLogic::createYes(), ], 5 => [ - new ObjectType(\Traversable::class), - new ObjectType(\Iterator::class), + new ObjectType(Traversable::class), + new ObjectType(Iterator::class), TrinaryLogic::createYes(), ], 6 => [ - new ObjectType(\ArrayObject::class), - new ObjectType(\Traversable::class), + new ObjectType(ArrayObject::class), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 7 => [ - new ObjectType(\Iterator::class), - new ObjectType(\Traversable::class), + new ObjectType(Iterator::class), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 8 => [ - new ObjectType(\ArrayObject::class), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(ArrayObject::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createNo(), ], 9 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new StringType(), ]), TrinaryLogic::createMaybe(), ], 10 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new ObjectType(\ArrayObject::class), + new ObjectType(ArrayObject::class), new StringType(), ]), TrinaryLogic::createNo(), ], 11 => [ - new ObjectType(\LogicException::class), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(LogicException::class), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createYes(), ], 12 => [ - new ObjectType(\InvalidArgumentException::class), - new ObjectType(\LogicException::class), + new ObjectType(InvalidArgumentException::class), + new ObjectType(LogicException::class), TrinaryLogic::createMaybe(), ], 13 => [ - new ObjectType(\ArrayAccess::class), - new StaticType(\Traversable::class), + new ObjectType(ArrayAccess::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 14 => [ - new ObjectType(\Countable::class), - new StaticType(\Countable::class), + new ObjectType(Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), TrinaryLogic::createYes(), ], 15 => [ - new ObjectType(\DateTimeImmutable::class), - new StaticType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createYes(), ], 16 => [ - new ObjectType(\Traversable::class), - new StaticType(\ArrayObject::class), + new ObjectType(Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), TrinaryLogic::createYes(), ], 17 => [ - new ObjectType(\Traversable::class), - new StaticType(\Iterator::class), + new ObjectType(Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), TrinaryLogic::createYes(), ], 18 => [ - new ObjectType(\ArrayObject::class), - new StaticType(\Traversable::class), + new ObjectType(ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 19 => [ - new ObjectType(\Iterator::class), - new StaticType(\Traversable::class), + new ObjectType(Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 20 => [ - new ObjectType(\ArrayObject::class), - new StaticType(\DateTimeImmutable::class), + new ObjectType(ArrayObject::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createNo(), ], 21 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new StringType(), ]), TrinaryLogic::createMaybe(), ], 22 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new StaticType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), new StringType(), ]), TrinaryLogic::createNo(), ], 23 => [ - new ObjectType(\LogicException::class), - new StaticType(\InvalidArgumentException::class), + new ObjectType(LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 24 => [ - new ObjectType(\InvalidArgumentException::class), - new StaticType(\LogicException::class), + new ObjectType(InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), TrinaryLogic::createMaybe(), ], 25 => [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createNo(), ], 26 => [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], 27 => [ - new ObjectType(\Countable::class), + new ObjectType(Countable::class), new IterableType(new MixedType(), new MixedType()), TrinaryLogic::createMaybe(), ], 28 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new HasMethodType('format'), TrinaryLogic::createMaybe(), ], 29 => [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new HasMethodType('format'), TrinaryLogic::createNo(), ], 30 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ new HasMethodType('format'), new HasMethodType('getTimestamp'), @@ -237,17 +297,17 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], 31 => [ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new HasPropertyType('d'), TrinaryLogic::createMaybe(), ], 32 => [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new HasPropertyType('d'), - TrinaryLogic::createNo(), + PHP_VERSION_ID < 80200 ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(), ], 33 => [ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new UnionType([ new HasPropertyType('d'), new HasPropertyType('m'), @@ -266,92 +326,167 @@ public function dataIsSuperTypeOf(): array ], 36 => [ new ObjectType('Exception'), - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createMaybe(), ], 37 => [ - new ObjectType(\InvalidArgumentException::class), + new ObjectType(InvalidArgumentException::class), new ObjectWithoutClassType(new ObjectType('Exception')), TrinaryLogic::createNo(), ], 38 => [ - new ObjectType(\Throwable::class, new ObjectType(\InvalidArgumentException::class)), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], 39 => [ - new ObjectType(\Throwable::class, new ObjectType(\InvalidArgumentException::class)), + new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), new ObjectType('Exception'), - TrinaryLogic::createYes(), + TrinaryLogic::createMaybe(), ], 40 => [ - new ObjectType(\Throwable::class, new ObjectType('Exception')), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], 41 => [ - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class, new ObjectType('Exception')), new ObjectType('Exception'), TrinaryLogic::createNo(), ], 42 => [ - new ObjectType(\Throwable::class, new ObjectType('Exception')), - new ObjectType(\Throwable::class), - TrinaryLogic::createYes(), + new ObjectType(Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class), + TrinaryLogic::createMaybe(), ], 43 => [ - new ObjectType(\Throwable::class), - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), TrinaryLogic::createYes(), ], 44 => [ - new ObjectType(\Throwable::class), - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), TrinaryLogic::createYes(), ], 45 => [ new ObjectType('Exception'), - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class, new ObjectType('Exception')), TrinaryLogic::createNo(), ], 46 => [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], 47 => [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTime::class), + TemplateTypeScope::createWithClass(DateTime::class), 'T', - new ObjectType(\DateTime::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTime::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], 48 => [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createMaybe(), ], + 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(), + ], ]; } /** * @dataProvider dataIsSuperTypeOf - * @param ObjectType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ObjectType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -359,7 +494,7 @@ public function testIsSuperTypeOf(ObjectType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -367,65 +502,72 @@ public function dataAccepts(): array { return [ [ - new ObjectType(\SimpleXMLElement::class), + new ObjectType(SimpleXMLElement::class), new IntegerType(), TrinaryLogic::createNo(), ], [ - new ObjectType(\SimpleXMLElement::class), + new ObjectType(SimpleXMLElement::class), new ConstantStringType('foo'), TrinaryLogic::createNo(), ], [ - new ObjectType(\Traversable::class), - new GenericObjectType(\Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new ObjectType(Traversable::class), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), TrinaryLogic::createYes(), ], [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), - TrinaryLogic::createMaybe(), + TrinaryLogic::createNo(), + ], + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createNo(), + ], + 63 => [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + TrinaryLogic::createNo(), ], ]; } /** * @dataProvider dataAccepts - * @param \PHPStan\Type\ObjectType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( ObjectType $type, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + $type->accepts($acceptedType, true)->result->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } public function testGetClassReflectionOfGenericClass(): void { - $objectType = new ObjectType(\Traversable::class); + $objectType = new ObjectType(Traversable::class); $classReflection = $objectType->getClassReflection(); $this->assertNotNull($classReflection); $this->assertSame('Traversable', $classReflection->getDisplayName()); @@ -435,43 +577,53 @@ public function dataHasOffsetValueType(): array { return [ [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new ObjectType(Generator::class), new IntegerType(), TrinaryLogic::createNo(), ], [ - new ObjectType(\ArrayAccess::class), + new ObjectType(ArrayAccess::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new ObjectType(Countable::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new MixedType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new StringType(), TrinaryLogic::createNo(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTimeInterface::class), new MixedType()]), - new ObjectType(\DateTime::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTimeInterface::class), new MixedType()]), + new ObjectType(DateTime::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTime::class), new MixedType()]), - new ObjectType(\DateTimeInterface::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTime::class), new MixedType()]), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTime::class), new MixedType()]), - new ObjectType(\stdClass::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTime::class), new MixedType()]), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ], ]; @@ -479,21 +631,93 @@ public function dataHasOffsetValueType(): array /** * @dataProvider dataHasOffsetValueType - * @param \PHPStan\Type\ObjectType $type - * @param Type $offsetType - * @param TrinaryLogic $expectedResult */ public function testHasOffsetValueType( ObjectType $type, Type $offsetType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), $type->hasOffsetValueType($offsetType)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $offsetType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $offsetType->describe(VerbosityLevel::precise())), ); } + 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/ObjectWithoutClassTypeTest.php b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php index 12a2c179ae..c5fde900ce 100644 --- a/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php +++ b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use function sprintf; -class ObjectWithoutClassTypeTest extends \PHPStan\Testing\TestCase +class ObjectWithoutClassTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -26,13 +29,13 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createNo(), ], [ - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), new ObjectType('Exception'), TrinaryLogic::createMaybe(), ], [ new ObjectWithoutClassType(new ObjectType('Exception')), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], [ @@ -46,13 +49,13 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], [ - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), new ObjectWithoutClassType(new ObjectType('Exception')), TrinaryLogic::createYes(), ], [ new ObjectWithoutClassType(new ObjectType('Exception')), - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createMaybe(), ], ]; @@ -60,9 +63,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param ObjectWithoutClassType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ObjectWithoutClassType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -70,7 +70,7 @@ public function testIsSuperTypeOf(ObjectWithoutClassType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/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 01bd796306..c681142840 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -2,26 +2,45 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use ArrayAccess; +use ArrayObject; +use Countable; +use DateTimeImmutable; +use Exception; +use InvalidArgumentException; +use Iterator; +use LogicException; +use PHPStan\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; +use stdClass; +use Traversable; +use function sprintf; -class StaticTypeTest extends \PHPStan\Testing\TestCase +class StaticTypeTest extends PHPStanTestCase { public function dataIsIterable(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ - [new StaticType('ArrayObject'), TrinaryLogic::createYes()], - [new StaticType('Traversable'), TrinaryLogic::createYes()], - [new StaticType('Unknown'), TrinaryLogic::createMaybe()], - [new StaticType('DateTime'), TrinaryLogic::createNo()], + [new StaticType($reflectionProvider->getClass('ArrayObject')), TrinaryLogic::createYes()], + [new StaticType($reflectionProvider->getClass('Traversable')), TrinaryLogic::createYes()], + [new StaticType($reflectionProvider->getClass('DateTime')), TrinaryLogic::createNo()], ]; } /** * @dataProvider dataIsIterable - * @param StaticType $type - * @param TrinaryLogic $expectedResult */ public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): void { @@ -29,23 +48,22 @@ public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), ); } public function dataIsCallable(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ - [new StaticType('Closure'), TrinaryLogic::createYes()], - [new StaticType('Unknown'), TrinaryLogic::createMaybe()], - [new StaticType('DateTime'), TrinaryLogic::createMaybe()], + [new StaticType($reflectionProvider->getClass('Closure')), TrinaryLogic::createYes()], + [new StaticType($reflectionProvider->getClass('DateTime')), TrinaryLogic::createMaybe()], ]; } /** * @dataProvider dataIsCallable - * @param StaticType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): void { @@ -53,187 +71,228 @@ public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } public function dataIsSuperTypeOf(): array { - $broker = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); return [ - 0 => [ - new StaticType('UnknownClassA'), - new ObjectType('UnknownClassB'), - TrinaryLogic::createMaybe(), - ], 1 => [ - new StaticType(\ArrayAccess::class), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayAccess::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 2 => [ - new StaticType(\Countable::class), - new ObjectType(\Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), + new ObjectType(Countable::class), TrinaryLogic::createMaybe(), ], 3 => [ - new StaticType(\DateTimeImmutable::class), - new ObjectType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createMaybe(), ], 4 => [ - new StaticType(\Traversable::class), - new ObjectType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new ObjectType(ArrayObject::class), TrinaryLogic::createMaybe(), ], 5 => [ - new StaticType(\Traversable::class), - new ObjectType(\Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new ObjectType(Iterator::class), TrinaryLogic::createMaybe(), ], 6 => [ - new StaticType(\ArrayObject::class), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 7 => [ - new StaticType(\Iterator::class), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 8 => [ - new StaticType(\ArrayObject::class), - new ObjectType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createNo(), ], 9 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new StringType(), ]), TrinaryLogic::createMaybe(), ], 10 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new ObjectType(\ArrayObject::class), + new ObjectType(ArrayObject::class), new StringType(), ]), TrinaryLogic::createNo(), ], 11 => [ - new StaticType(\LogicException::class), - new ObjectType(\InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createMaybe(), ], 12 => [ - new StaticType(\InvalidArgumentException::class), - new ObjectType(\LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), + new ObjectType(LogicException::class), TrinaryLogic::createMaybe(), ], 13 => [ - new StaticType(\ArrayAccess::class), - new StaticType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayAccess::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 14 => [ - new StaticType(\Countable::class), - new StaticType(\Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), + new StaticType($reflectionProvider->getClass(Countable::class)), TrinaryLogic::createYes(), ], 15 => [ - new StaticType(\DateTimeImmutable::class), - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createYes(), ], 16 => [ - new StaticType(\Traversable::class), - new StaticType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), TrinaryLogic::createYes(), ], 17 => [ - new StaticType(\Traversable::class), - new StaticType(\Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new StaticType($reflectionProvider->getClass(Iterator::class)), TrinaryLogic::createYes(), ], 18 => [ - new StaticType(\ArrayObject::class), - new StaticType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 19 => [ - new StaticType(\Iterator::class), - new StaticType(\Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 20 => [ - new StaticType(\ArrayObject::class), - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createNo(), ], 21 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType(\DateTimeImmutable::class), - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), ]), TrinaryLogic::createYes(), ], 22 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new StringType(), ]), TrinaryLogic::createMaybe(), ], 23 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), new StringType(), ]), TrinaryLogic::createNo(), ], 24 => [ - new StaticType(\LogicException::class), - new StaticType(\InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 25 => [ - new StaticType(\InvalidArgumentException::class), - new StaticType(\LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), + new StaticType($reflectionProvider->getClass(LogicException::class)), TrinaryLogic::createMaybe(), ], 26 => [ - new StaticType(\stdClass::class), + new StaticType($reflectionProvider->getClass(stdClass::class)), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 27 => [ new ObjectWithoutClassType(), - new StaticType(\stdClass::class), + new StaticType($reflectionProvider->getClass(stdClass::class)), TrinaryLogic::createYes(), ], 28 => [ - new ThisType($broker->getClass(\stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 29 => [ new ObjectWithoutClassType(), - new ThisType($broker->getClass(\stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), + TrinaryLogic::createYes(), + ], + [ + new StaticType($reflectionProvider->getClass(Base::class)), + new ObjectType(Child::class), + TrinaryLogic::createMaybe(), + ], + [ + new StaticType($reflectionProvider->getClass(Base::class)), + new StaticType($reflectionProvider->getClass(FinalChild::class)), + TrinaryLogic::createYes(), + ], + [ + new StaticType($reflectionProvider->getClass(Base::class)), + new StaticType($reflectionProvider->getClass(Child::class)), + TrinaryLogic::createYes(), + ], + [ + new StaticType($reflectionProvider->getClass(Base::class)), + 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(), + ], ]; } /** * @dataProvider dataIsSuperTypeOf - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -241,33 +300,33 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataEquals(): array { - $reflectionProvider = Broker::getInstance(); + $reflectionProvider = $this->createReflectionProvider(); return [ [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new ThisType($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), true, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new ThisType($reflectionProvider->getClass(\InvalidArgumentException::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), + new ThisType($reflectionProvider->getClass(InvalidArgumentException::class)), false, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new StaticType(\Exception::class), + new ThisType($reflectionProvider->getClass(Exception::class)), + new StaticType($reflectionProvider->getClass(Exception::class)), false, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new StaticType(\InvalidArgumentException::class), + new ThisType($reflectionProvider->getClass(Exception::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), false, ], ]; @@ -275,9 +334,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param StaticType $type - * @param StaticType $otherType - * @param bool $expected */ public function testEquals(StaticType $type, StaticType $otherType, bool $expected): void { @@ -285,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 f22ad362d5..813349b91a 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -2,13 +2,20 @@ namespace PHPStan\Type; -use PHPStan\Testing\TestCase; +use Exception; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasPropertyType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use stdClass; use Test\ClassWithToString; +use function sprintf; -class StringTypeTest extends TestCase +class StringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -16,7 +23,72 @@ public function dataIsSuperTypeOf(): array return [ [ new StringType(), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + TrinaryLogic::createYes(), + ], + [ + new StringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new ClassStringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new ClassStringType(), + TrinaryLogic::createMaybe(), + ], + [ + new GenericClassStringType(new ObjectType(stdClass::class)), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new GenericClassStringType(new ObjectType(stdClass::class)), + TrinaryLogic::createMaybe(), + ], + [ + new StringAlwaysAcceptingObjectWithToStringType(), + new ObjectType(ClassWithToString::class), TrinaryLogic::createYes(), ], ]; @@ -31,7 +103,7 @@ public function testIsSuperTypeOf(StringType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -51,21 +123,116 @@ public function dataAccepts(): iterable new ClassStringType(), TrinaryLogic::createYes(), ]; + + yield [ + new StringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new StringType(), + TrinaryLogic::createYes(), + ]; + + yield [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + new StringType(), + TrinaryLogic::createMaybe(), + ]; + + yield [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + TrinaryLogic::createYes(), + ]; } /** * @dataProvider dataAccepts - * @param \PHPStan\Type\StringType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ 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(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataEquals(): array + { + return [ + [ + new StringType(), + new StringType(), + true, + ], + [ + new ConstantStringType('foo'), + new ConstantStringType('foo'), + true, + ], + [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + false, + ], + [ + new StringType(), + new ConstantStringType(''), + false, + ], + [ + new ConstantStringType(''), + new StringType(), + false, + ], + [ + new StringType(), + new ClassStringType(), + false, + ], + ]; + } + + /** + * @dataProvider dataEquals + */ + public function testEquals(StringType $type, Type $otherType, bool $expectedResult): void + { + $actualResult = $type->equals($otherType); + $this->assertSame( + $expectedResult, + $actualResult, + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/TemplateTypeTest.php b/tests/PHPStan/Type/TemplateTypeTest.php index 12b16d5a5e..6b184eef8a 100644 --- a/tests/PHPStan/Type/TemplateTypeTest.php +++ b/tests/PHPStan/Type/TemplateTypeTest.php @@ -2,59 +2,68 @@ namespace PHPStan\Type; +use DateTime; +use DateTimeInterface; +use Exception; +use Iterator; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use stdClass; +use Throwable; +use Traversable; +use function array_map; +use function assert; +use function sprintf; -class TemplateTypeTest extends \PHPStan\Testing\TestCase +class TemplateTypeTest extends PHPStanTestCase { public function dataAccepts(): array { - $templateType = static function (string $name, ?Type $bound, ?string $functionName = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction($functionName ?? '_'), - $name, - $bound, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction($functionName ?? '_'), + $name, + $bound, + TemplateTypeVariance::createInvariant(), + ); return [ - [ + 0 => [ $templateType('T', new ObjectType('DateTime')), new ObjectType('DateTime'), TrinaryLogic::createYes(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], - [ + 1 => [ $templateType('T', new ObjectType('DateTimeInterface')), new ObjectType('DateTime'), TrinaryLogic::createYes(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], - [ + 2 => [ $templateType('T', new ObjectType('DateTime')), $templateType('T', new ObjectType('DateTime')), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 3 => [ $templateType('T', new ObjectType('DateTime'), 'a'), $templateType('T', new ObjectType('DateTime'), 'b'), TrinaryLogic::createMaybe(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], - [ + 4 => [ $templateType('T', null), new MixedType(), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 5 => [ $templateType('T', null), new IntersectionType([ new ObjectWithoutClassType(), @@ -63,6 +72,27 @@ public function dataAccepts(): array TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], + 'accepts itself with a sub-type union bound' => [ + $templateType('T', new UnionType([ + new IntegerType(), + new StringType(), + ])), + $templateType('T', new IntegerType()), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 'accepts itself with a sub-type object bound' => [ + $templateType('T', new ObjectWithoutClassType()), + $templateType('T', new ObjectType('stdClass')), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 'does not accept ObjectType that is a super type of bound' => [ + $templateType('T', new ObjectType(Iterator::class)), + new ObjectType(Traversable::class), + TrinaryLogic::createNo(), + TrinaryLogic::createNo(), + ], ]; } @@ -73,38 +103,36 @@ public function testAccepts( Type $type, Type $otherType, TrinaryLogic $expectedAccept, - TrinaryLogic $expectedAcceptArg + TrinaryLogic $expectedAcceptArg, ): void { assert($type instanceof TemplateType); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedAccept->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); $type = $type->toArgument(); - $actualResult = $type->accepts($otherType, true); + $actualResult = $type->accepts($otherType, true)->result; $this->assertSame( $expectedAcceptArg->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s) (Argument strategy)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s) (Argument strategy)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataIsSuperTypeOf(): array { - $templateType = static function (string $name, ?Type $bound, ?string $functionName = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction($functionName ?? '_'), - $name, - $bound, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction($functionName ?? '_'), + $name, + $bound, + TemplateTypeVariance::createInvariant(), + ); return [ 0 => [ @@ -141,7 +169,7 @@ public function dataIsSuperTypeOf(): array $templateType('T', new ObjectType('DateTime')), $templateType('T', new ObjectType('DateTimeInterface')), TrinaryLogic::createMaybe(), // (T of DateTime) isSuperTypeTo (T of DateTimeInterface) - TrinaryLogic::createMaybe(), // (T of DateTimeInterface) isSuperTypeTo (T of DateTime) + TrinaryLogic::createYes(), // (T of DateTimeInterface) isSuperTypeTo (T of DateTime) ], 6 => [ $templateType('T', new ObjectType('DateTime')), @@ -184,7 +212,7 @@ public function dataIsSuperTypeOf(): array ], 11 => [ $templateType('T', null), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), // T isSuperTypeTo DateTimeInterface TrinaryLogic::createMaybe(), // DateTimeInterface isSuperTypeTo T ], @@ -196,16 +224,64 @@ public function dataIsSuperTypeOf(): array ], 13 => [ $templateType('T', new ObjectWithoutClassType()), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], 14 => [ - $templateType('T', new ObjectType(\Throwable::class)), - new ObjectType(\Exception::class), + $templateType('T', new ObjectType(Throwable::class)), + new ObjectType(Exception::class), + TrinaryLogic::createMaybe(), + TrinaryLogic::createMaybe(), + ], + 15 => [ + $templateType('T', new MixedType(true)), + $templateType('U', new UnionType([new IntegerType(), new StringType()])), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], + 16 => [ + $templateType('T', new MixedType(true)), + $templateType('U', new BenevolentUnionType([new IntegerType(), new StringType()])), + TrinaryLogic::createMaybe(), + TrinaryLogic::createMaybe(), + ], + 17 => [ + $templateType('T', new ObjectType(stdClass::class)), + $templateType('U', new BenevolentUnionType([new IntegerType(), new StringType()])), + TrinaryLogic::createNo(), + TrinaryLogic::createNo(), + ], + 18 => [ + $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), + $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 19 => [ + $templateType('T', new UnionType([new IntegerType(), new StringType()])), + $templateType('T', new UnionType([new IntegerType(), new StringType()])), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 20 => [ + $templateType('T', new UnionType([new IntegerType(), new StringType()])), + $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 21 => [ + $templateType('T', new UnionType([new IntegerType(), new StringType()])), + $templateType('T', new IntegerType()), + TrinaryLogic::createYes(), + TrinaryLogic::createMaybe(), + ], + 22 => [ + $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), + new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType(), new NullType()]), + TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), + ], ]; } @@ -216,7 +292,7 @@ public function testIsSuperTypeOf( Type $type, Type $otherType, TrinaryLogic $expectedIsSuperType, - TrinaryLogic $expectedIsSuperTypeInverse + TrinaryLogic $expectedIsSuperTypeInverse, ): void { assert($type instanceof TemplateType); @@ -225,29 +301,26 @@ public function testIsSuperTypeOf( $this->assertSame( $expectedIsSuperType->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); $actualResult = $otherType->isSuperTypeOf($type); $this->assertSame( $expectedIsSuperTypeInverse->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static function (string $name, ?Type $bound = null, ?string $functionName = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction($functionName ?? '_'), - $name, - $bound, - TemplateTypeVariance::createInvariant() - ); - }; - + $templateType = static fn ($name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction($functionName ?? '_'), + $name, + $bound, + TemplateTypeVariance::createInvariant(), + ); return [ 'simple' => [ new IntegerType(), @@ -255,33 +328,33 @@ public function dataInferTemplateTypes(): array ['T' => 'int'], ], 'object' => [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), $templateType('T'), ['T' => 'DateTime'], ], 'object with bound' => [ - new ObjectType(\DateTime::class), - $templateType('T', new ObjectType(\DateTimeInterface::class)), + new ObjectType(DateTime::class), + $templateType('T', new ObjectType(DateTimeInterface::class)), ['T' => 'DateTime'], ], 'wrong object with bound' => [ - new ObjectType(\stdClass::class), - $templateType('T', new ObjectType(\DateTimeInterface::class)), + new ObjectType(stdClass::class), + $templateType('T', new ObjectType(DateTimeInterface::class)), [], ], 'template type' => [ - TemplateTypeHelper::toArgument($templateType('T', new ObjectType(\DateTimeInterface::class))), - $templateType('T', new ObjectType(\DateTimeInterface::class)), + TemplateTypeHelper::toArgument($templateType('T', new ObjectType(DateTimeInterface::class))), + $templateType('T', new ObjectType(DateTimeInterface::class)), ['T' => 'T of DateTimeInterface (function _(), argument)'], ], 'foreign template type' => [ - TemplateTypeHelper::toArgument($templateType('T', new ObjectType(\DateTimeInterface::class), 'a')), - $templateType('T', new ObjectType(\DateTimeInterface::class), 'b'), + TemplateTypeHelper::toArgument($templateType('T', new ObjectType(DateTimeInterface::class), 'a')), + $templateType('T', new ObjectType(DateTimeInterface::class), 'b'), ['T' => 'T of DateTimeInterface (function a(), argument)'], ], 'foreign template type, imcompatible bound' => [ - TemplateTypeHelper::toArgument($templateType('T', new ObjectType(\stdClass::class), 'a')), - $templateType('T', new ObjectType(\DateTime::class), 'b'), + TemplateTypeHelper::toArgument($templateType('T', new ObjectType(stdClass::class), 'a')), + $templateType('T', new ObjectType(DateTime::class), 'b'), [], ], ]; @@ -297,9 +370,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php index 1e82f949a5..5d34d6f911 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php @@ -3,13 +3,14 @@ namespace PHPStan\Type; use PHPStan\Fixture\TestDecimal; +use function in_array; final class TestDecimalOperatorTypeSpecifyingExtension implements OperatorTypeSpecifyingExtension { 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 546b79a7b2..55f44ec4fe 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php @@ -4,6 +4,7 @@ use PHPStan\Fixture\TestDecimal; use PHPUnit\Framework\TestCase; +use stdClass; class TestDecimalOperatorTypeSpecifyingExtensionTest extends TestCase { @@ -45,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), + ]; } /** @@ -63,14 +76,14 @@ public function dataNotMatchingSidesProvider(): iterable { yield 'left' => [ '+', - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new ObjectType(TestDecimal::class), ]; yield 'right' => [ '+', new ObjectType(TestDecimal::class), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), ]; } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 268fdc27a2..a85b6a4709 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2,26 +2,67 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +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; 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\GenericStaticType; +use PHPStan\Type\Generic\TemplateBenevolentUnionType; +use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateObjectType; use PHPStan\Type\Generic\TemplateObjectWithoutClassType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use RecursionCallable\Foo; +use stdClass; +use Test\ClassWithNullableProperty; +use Test\ClassWithToString; +use Test\FirstInterface; +use Throwable; +use Traversable; +use function array_map; +use function array_reverse; +use function get_class; +use function implode; +use function sprintf; +use const PHP_VERSION_ID; -class TypeCombinatorTest extends \PHPStan\Testing\TestCase +class TypeCombinatorTest extends PHPStanTestCase { public function dataAddNull(): array @@ -88,14 +129,12 @@ public function dataAddNull(): array /** * @dataProvider dataAddNull - * @param \PHPStan\Type\Type $type - * @param string $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testAddNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::addNull($type); @@ -105,14 +144,12 @@ public function testAddNull( /** * @dataProvider dataAddNull - * @param \PHPStan\Type\Type $type - * @param string $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testUnionWithNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::union($type, new NullType()); @@ -122,7 +159,8 @@ public function testUnionWithNull( public function dataRemoveNull(): array { - $reflectionProvider = Broker::getInstance(); + $reflectionProvider = $this->createReflectionProvider(); + return [ [ new MixedType(), @@ -182,7 +220,7 @@ public function dataRemoveNull(): array ], [ new UnionType([ - new ThisType($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), new NullType(), ]), ThisType::class, @@ -190,7 +228,7 @@ public function dataRemoveNull(): array ], [ new UnionType([ - new ThisType(\Exception::class), + new ThisType($reflectionProvider->getClass(Exception::class)), new NullType(), ]), ThisType::class, @@ -209,14 +247,12 @@ public function dataRemoveNull(): array /** * @dataProvider dataRemoveNull - * @param \PHPStan\Type\Type $type - * @param string $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testRemoveNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::removeNull($type); @@ -224,9 +260,9 @@ public function testRemoveNull( $this->assertInstanceOf($expectedTypeClass, $result); } - public function dataUnion(): array + public function dataUnion(): iterable { - return [ + yield from [ [ [ new StringType(), @@ -545,7 +581,7 @@ public function dataUnion(): array ], [ [ - new ObjectType(\RecursionCallable\Foo::class), + new ObjectType(Foo::class), new CallableType(), ], UnionType::class, @@ -639,6 +675,14 @@ public function dataUnion(): array ConstantBooleanType::class, 'true', ], + [ + [ + new ConstantBooleanType(false), + new ConstantBooleanType(false), + ], + ConstantBooleanType::class, + 'false', + ], [ [ new ConstantBooleanType(true), @@ -649,7 +693,7 @@ public function dataUnion(): array ], [ [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), ], ObjectType::class, @@ -670,7 +714,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -681,8 +725,8 @@ public function dataUnion(): array new StringType(), ]), ], - ConstantArrayType::class, - 'array(\'foo\' => DateTimeImmutable|null, \'bar\' => int|string)', + UnionType::class, + 'array{foo: DateTimeImmutable, bar: int}|array{foo: null, bar: string}', ], [ [ @@ -690,7 +734,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -699,8 +743,8 @@ public function dataUnion(): array new NullType(), ]), ], - ConstantArrayType::class, - 'array(\'foo\' => DateTimeImmutable|null, ?\'bar\' => int)', + UnionType::class, + 'array{foo: DateTimeImmutable, bar: int}|array{foo: null}', ], [ [ @@ -708,7 +752,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -721,20 +765,20 @@ public function dataUnion(): array 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}', ], [ [ new ArrayType( new IntegerType(), - new ObjectType(\stdClass::class) + new ObjectType(stdClass::class), ), new ConstantArrayType([ new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), ], @@ -830,22 +874,8 @@ public function dataUnion(): array new ConstantStringType('loremm'), new ConstantStringType('loremmm'), ], - StringType::class, - 'string', - ], - [ - [ - 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)', + UnionType::class, + "'bar'|'barr'|'baz'|'bazz'|'foo'|'fooo'|'lorem'|'loremm'|'loremmm'", ], [ [ @@ -872,7 +902,7 @@ public function dataUnion(): array [ new ObjectWithoutClassType(), new ConstantStringType('foo'), - ] + ], ), new CallableType(), ]), @@ -885,13 +915,13 @@ public function dataUnion(): array [ new ObjectWithoutClassType(), new ConstantStringType('foo'), - ] + ], ), new CallableType(), ]), ], IntersectionType::class, - 'array(object, \'foo\')&callable(): mixed', + 'array{object, \'foo\'}&callable(): mixed', ], [ [ @@ -923,8 +953,8 @@ public function dataUnion(): array new HasOffsetType(new ConstantStringType('bar')), ]), ], - ArrayType::class, - 'array', + IntersectionType::class, + 'non-empty-array', ], [ [ @@ -939,7 +969,21 @@ public function dataUnion(): array ]), ], 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', ], [ [ @@ -1039,7 +1083,7 @@ public function dataUnion(): array new ObjectWithoutClassType(new ObjectType('A')), ], MixedType::class, - 'mixed=implicit', + 'mixed~int=implicit', ], [ [ @@ -1095,7 +1139,7 @@ public function dataUnion(): array new ObjectType('InvalidArgumentException'), ], MixedType::class, - 'mixed=implicit', // should be MixedType~Exception+InvalidArgumentException + 'mixed~Exception~InvalidArgumentException=implicit', ], [ [ @@ -1119,7 +1163,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -1132,7 +1176,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -1145,13 +1189,13 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], TemplateType::class, @@ -1163,18 +1207,124 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'U', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, 'T of DateTime (function a(), parameter)|U of DateTime (function a(), parameter)', ], + 'bug6210-1' => [ + [ + new ObjectWithoutClassType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new HasMethodType('getId'), + ]), + ], + ObjectWithoutClassType::class, + 'object', + ], + 'bug6210-2' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new HasMethodType('getId'), + ]), + ], + TemplateMixedType::class, + 'T (function a(), parameter)=explicit', + ], + 'bug6210-3' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new HasMethodType('getId'), + ]), + ], + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', + ], + 'bug6210-4' => [ + [ + new ObjectWithoutClassType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType('getId'), + ]), + ], + ObjectWithoutClassType::class, + 'object', + ], + 'bug6210-5' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new HasPropertyType('getId'), + ]), + ], + TemplateMixedType::class, + 'T (function a(), parameter)=explicit', + ], + 'bug6210-6' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new HasPropertyType('getId'), + ]), + ], + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', + ], [ [ new BenevolentUnionType([new IntegerType(), new StringType()]), @@ -1260,7 +1410,7 @@ public function dataUnion(): array [ [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), ], ClassStringType::class, 'class-string', @@ -1283,15 +1433,15 @@ public function dataUnion(): array ], [ [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new ClassStringType(), ], ClassStringType::class, @@ -1299,7 +1449,7 @@ public function dataUnion(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), ], StringType::class, @@ -1307,68 +1457,92 @@ public function dataUnion(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), ], UnionType::class, 'class-string|class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), ], UnionType::class, '\'Exception\'|class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\stdClass::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), ], UnionType::class, '\'stdClass\'|class-string', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new CallableType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new CallableType()]), + new ConstantStringType('test_function'), + ], + UnionType::class, + '\'test_function\'|callable-string', + ], + [ + [ + new IntersectionType([new StringType(), new CallableType()]), + new IntegerType(), + ], + UnionType::class, + 'callable-string|int', + ], [ [ IntegerRangeType::fromInterval(1, 3), @@ -1393,6 +1567,42 @@ public function dataUnion(): array UnionType::class, 'int<1, 3>|int<7, 9>', ], + [ + [ + IntegerRangeType::fromInterval(7, 9), + IntegerRangeType::fromInterval(1, 3), + ], + 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), @@ -1455,10 +1665,10 @@ public function dataUnion(): array [ [ new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), ], GenericObjectType::class, @@ -1467,10 +1677,10 @@ public function dataUnion(): array [ [ new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ], UnionType::class, @@ -1479,10 +1689,10 @@ public function dataUnion(): array [ [ new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ], GenericObjectType::class, @@ -1494,7 +1704,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectWithoutClassType(), ], @@ -1507,9 +1717,9 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), ], UnionType::class, 'stdClass|T of object (function a(), parameter)', @@ -1520,7 +1730,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -1533,13 +1743,13 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'K', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, @@ -1551,13 +1761,13 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'K', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, @@ -1568,14 +1778,14 @@ public function dataUnion(): array TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'K', - new ObjectType(\stdClass::class), - TemplateTypeVariance::createInvariant() + new ObjectType(stdClass::class), + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, @@ -1583,11 +1793,11 @@ public function dataUnion(): array ], [ [ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\DateTimeInterface::class, new ObjectType(\DateTimeImmutable::class)), + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTimeInterface::class, new ObjectType(DateTimeImmutable::class)), ], ObjectType::class, - \DateTimeInterface::class, + DateTimeInterface::class, ], [ [ @@ -1607,7 +1817,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array()|array(string)', + 'array{}|array{string}', ], [ [ @@ -1616,10 +1826,10 @@ public function dataUnion(): array new ConstantIntegerType(0), ], [ new StringType(), - ], 1, [0]), + ], [1], [0]), ], UnionType::class, - 'array()|array(?0 => string)', + 'array{}|array{0?: string}', ], [ [ @@ -1639,7 +1849,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array(\'a\' => int, \'b\' => int)|array(\'c\' => int, \'d\' => int)', + 'array{a: int, b: int}|array{c: int, d: int}', ], [ [ @@ -1657,7 +1867,7 @@ public function dataUnion(): array ]), ], ConstantArrayType::class, - 'array(\'a\' => int, ?\'b\' => int)', + 'array{a: int, b?: int}', ], [ [ @@ -1677,1076 +1887,2858 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array(\'a\' => int, \'b\' => int)|array(\'b\' => int, \'c\' => int)', + 'array{a: int, b: int}|array{b: int, c: int}', ], - ]; - } - - /** - * @dataProvider dataUnion - * @param \PHPStan\Type\Type[] $types - * @param string $expectedTypeClass - * @param string $expectedTypeDescription - */ - 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 function (Type $type): string { - return $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 \PHPStan\Type\Type[] $types - * @param string $expectedTypeClass - * @param string $expectedTypeDescription - */ - 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 function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, - $types - ))) - ); - $this->assertInstanceOf($expectedTypeClass, $actualType); - } - - public function dataIntersect(): array - { - return [ [ [ - new IterableType(new MixedType(), new StringType()), - new ObjectType('ArrayObject'), + StaticTypeFactory::falsey(), + StaticTypeFactory::falsey(), ], - IntersectionType::class, - 'ArrayObject&iterable', + UnionType::class, + '0|0.0|\'\'|\'0\'|array{}|false|null', ], [ [ - new IterableType(new MixedType(), new StringType()), - new ArrayType(new MixedType(), new StringType()), + StaticTypeFactory::truthy(), + StaticTypeFactory::truthy(), ], - ArrayType::class, - 'array', + MixedType::class, + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)=implicit', ], [ [ - new IterableType(new MixedType(true), new StringType()), - new ObjectType('Iterator'), + StaticTypeFactory::falsey(), + StaticTypeFactory::truthy(), ], - IntersectionType::class, - 'iterable&Iterator', + MixedType::class, + 'mixed=implicit', ], [ [ - 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('Foo'), - ], - StaticType::class, - 'static(Foo)', - ], - [ - [ - new VoidType(), - new MixedType(), + TemplateTypeFactory::create(TemplateTypeScope::createWithFunction('foo'), 'T', new BenevolentUnionType([new IntegerType(), new StringType()]), TemplateTypeVariance::createInvariant()), ], - VoidType::class, - 'void', + TemplateBenevolentUnionType::class, + 'T of (int|string) (function foo(), parameter)', ], - [ [ - new ObjectType('UnknownClass'), - new ObjectType('UnknownClass'), + new ConstantStringType(''), + new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]), ], - ObjectType::class, - 'UnknownClass', + StringType::class, + 'string', ], [ [ - new UnionType([new ObjectType('UnknownClassA'), new ObjectType('UnknownClassB')]), - new UnionType([new ObjectType('UnknownClassA'), new ObjectType('UnknownClassB')]), + new ConstantStringType('0'), + new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]), ], - UnionType::class, - 'UnknownClassA|UnknownClassB', + IntersectionType::class, + 'non-empty-string', ], [ [ - new ConstantBooleanType(true), - new BooleanType(), + new ConstantStringType(''), + new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]), ], - ConstantBooleanType::class, - 'true', + StringType::class, + 'string', ], [ [ new StringType(), - new NeverType(), - ], - NeverType::class, - '*NEVER*', - ], - [ - [ - new ObjectType('Iterator'), - new ObjectType('Countable'), - new ObjectType('Traversable'), - ], - IntersectionType::class, - 'Countable&Iterator', - ], - [ - [ - new ObjectType('Iterator'), - new ObjectType('Traversable'), - new ObjectType('Countable'), + new UnionType([ + new ConstantStringType(''), + new ConstantStringType('0'), + new ConstantBooleanType(false), + ]), ], - IntersectionType::class, - 'Countable&Iterator', + UnionType::class, + 'string|false', ], [ [ - new ObjectType('Traversable'), - new ObjectType('Iterator'), - new ObjectType('Countable'), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new UnionType([ + new ConstantStringType(''), + new ConstantStringType('0'), + new ConstantBooleanType(false), + ]), ], - IntersectionType::class, - 'Countable&Iterator', + UnionType::class, + 'string|false', ], [ [ - new IterableType(new MixedType(), new MixedType()), - new IterableType(new MixedType(), new StringType()), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), ], - IterableType::class, - 'iterable', + UnionType::class, + 'literal-string|numeric-string', ], [ [ - new ArrayType(new MixedType(), new MixedType()), - new IterableType(new MixedType(), new StringType()), + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - IntersectionType::class, - 'array&iterable', // this is correct but 'array' would be better + StringType::class, + 'string', ], [ [ - new MixedType(), - new IterableType(new MixedType(), new MixedType()), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - IterableType::class, - 'iterable', + UnionType::class, + 'lowercase-string|numeric-string', ], [ [ - new IntegerType(), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - IntegerType::class, - 'int', + UnionType::class, + 'lowercase-string|non-falsy-string', ], [ [ - new ConstantIntegerType(1), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - ConstantIntegerType::class, - '1', + UnionType::class, + 'lowercase-string|non-empty-string', ], [ [ - new ConstantStringType('foo'), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - ConstantStringType::class, - '\'foo\'', + UnionType::class, + 'literal-string|lowercase-string', ], [ [ new StringType(), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], StringType::class, 'string', ], [ [ - new UnionType([new StringType(), new IntegerType()]), - new BenevolentUnionType([new IntegerType(), new StringType()]), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], UnionType::class, - 'int|string', - ], - [ - [ - new ObjectType(\Test\Foo::class), - new HasMethodType('__toString'), - ], - IntersectionType::class, - 'Test\Foo&hasMethod(__toString)', + 'numeric-string|uppercase-string', ], [ [ - new ObjectType(\Test\ClassWithToString::class), - new HasMethodType('__toString'), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ObjectType::class, - 'Test\ClassWithToString', + UnionType::class, + 'non-falsy-string|uppercase-string', ], [ [ - new ObjectType(\CheckTypeFunctionCall\FinalClassWithMethodExists::class), - new HasMethodType('doBar'), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - NeverType::class, - '*NEVER*', + UnionType::class, + 'non-empty-string|uppercase-string', ], [ [ - new ObjectWithoutClassType(), - new HasMethodType('__toString'), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - IntersectionType::class, - 'object&hasMethod(__toString)', + UnionType::class, + 'literal-string|uppercase-string', ], [ [ - new IntegerType(), - new HasMethodType('__toString'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - NeverType::class, - '*NEVER*', + UnionType::class, + 'lowercase-string|uppercase-string', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType('__toString'), - ]), - new HasMethodType('__toString'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('doFoo'), + 'T', + new UnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + new BooleanType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), ], - IntersectionType::class, - 'object&hasMethod(__toString)', + UnionType::class, + '(T of bool|float|int|string (function doFoo(), parameter))|null', ], [ [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasMethodType('foo'), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), ]), - new HasMethodType('bar'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'TCode', + new UnionType([new ArrayType(new IntegerType(), new IntegerType()), new IntegerType()]), + TemplateTypeVariance::createInvariant(), + ), ], - IntersectionType::class, - 'object&hasMethod(bar)&hasMethod(foo)', + UnionType::class, + 'array|int|int<1, max>|(TCode of array|int (class Foo, parameter))', ], [ [ new UnionType([ - new ObjectType(\Test\Foo::class), - new ObjectType(\Test\FirstInterface::class), + new ArrayType(new MixedType(), new MixedType()), + new CallableType(), ]), - new HasMethodType('__toString'), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'TCode', + new UnionType([new ArrayType(new IntegerType(), new IntegerType()), new IntegerType()]), + TemplateTypeVariance::createInvariant(), + ), ], UnionType::class, - '(Test\FirstInterface&hasMethod(__toString))|(Test\Foo&hasMethod(__toString))', + 'array|(callable(): mixed)|(TCode of array|int (class Foo, parameter))', ], [ [ - new ObjectType(\Test\Foo::class), - new HasPropertyType('fooProperty'), + new MixedType(), + new StrictMixedType(), ], - IntersectionType::class, - 'Test\Foo&hasProperty(fooProperty)', + MixedType::class, + 'mixed=implicit', ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ [ - [ - new ObjectType(\Test\ClassWithNullableProperty::class), - new HasPropertyType('foo'), - ], - ObjectType::class, - 'Test\ClassWithNullableProperty', + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + ObjectType::class, + 'PHPStan\Fixture\TestEnum', + ]; + yield [ [ - [ - new ObjectType(\CheckTypeFunctionCall\FinalClassWithPropertyExists::class), - new HasPropertyType('barProperty'), - ], - NeverType::class, - '*NEVER*', + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum', + ]; + yield [ [ - [ - new ObjectWithoutClassType(), - new HasPropertyType('fooProperty'), - ], - IntersectionType::class, - 'object&hasProperty(fooProperty)', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ [ - [ - new IntegerType(), - new HasPropertyType('fooProperty'), - ], - NeverType::class, - '*NEVER*', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|PHPStan\Fixture\TestEnum::TWO', + ]; + yield [ [ - [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType('fooProperty'), - ]), - new HasPropertyType('fooProperty'), - ], - IntersectionType::class, - 'object&hasProperty(fooProperty)', + new ObjectType('PHPStan\Fixture\TestEnum'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ [ - [ - new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType('foo'), - ]), - new HasPropertyType('bar'), - ], - IntersectionType::class, - 'object&hasProperty(bar)&hasProperty(foo)', + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ [ - [ - new UnionType([ - new ObjectType(\Test\Foo::class), - new ObjectType(\Test\FirstInterface::class), - ]), - new HasPropertyType('fooProperty'), - ], - UnionType::class, - '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnumInterface', + ]; + yield [ [ - [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new ConstantStringType('a')), - ], - IntersectionType::class, - 'array&hasOffset(\'a\')', + new ObjectWithoutClassType(), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ [ - [ - new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new ConstantStringType('a')), - new HasOffsetType(new ConstantStringType('a')), - ], - IntersectionType::class, - 'array&hasOffset(\'a\')', + new ObjectType('stdClass'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|stdClass', + ]; + yield [ [ - [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new MixedType(false, IntegerRangeType::fromInterval(17, null)), + IntegerRangeType::fromInterval(19, null), + ], + MixedType::class, + 'mixed~int<17, 18>=implicit', + ]; + + $reflectionProvider = $this->createReflectionProvider(); + yield [ + [ + new StaticType($reflectionProvider->getClass(stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), + ], + StaticType::class, + 'static(stdClass)', + ]; + + yield [ + [ + new StaticType($reflectionProvider->getClass(stdClass::class)), + new ObjectType(stdClass::class), + ], + ObjectType::class, + 'stdClass', + ]; + + yield [ + [ + 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 IntersectionType([ + new ObjectWithoutClassType(), + new HasMethodType('foo'), + ]), + new HasMethodType('bar'), + ], + IntersectionType::class, + 'object&hasMethod(bar)&hasMethod(foo)', + ], + [ + [ + new UnionType([ + new ObjectType(\Test\Foo::class), + new ObjectType(FirstInterface::class), + ]), + new HasMethodType('__toString'), + ], + UnionType::class, + '(Test\FirstInterface&hasMethod(__toString))|(Test\Foo&hasMethod(__toString))', + ], + [ + [ + new ObjectType(\Test\Foo::class), + new HasPropertyType('fooProperty'), + ], + IntersectionType::class, + 'Test\Foo&hasProperty(fooProperty)', + ], + [ + [ + new ObjectType(FinalFoo::class), + new HasPropertyType('fooProperty'), + ], + PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, + PHP_VERSION_ID < 80200 ? 'DynamicProperties\FinalFoo&hasProperty(fooProperty)' : '*NEVER*=implicit', + ], + [ + [ + new ObjectType(ClassWithNullableProperty::class), + new HasPropertyType('foo'), + ], + ObjectType::class, + 'Test\ClassWithNullableProperty', + ], + [ + [ + 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, + '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', + ], + [ + [ + new UnionType([ + new ObjectType(FinalFoo::class), + new ObjectType(FirstInterface::class), + ]), + new HasPropertyType('fooProperty'), + ], + PHP_VERSION_ID < 80200 ? UnionType::class : IntersectionType::class, + PHP_VERSION_ID < 80200 ? '(DynamicProperties\FinalFoo&hasProperty(fooProperty))|(Test\FirstInterface&hasProperty(fooProperty))' : 'Test\FirstInterface&hasProperty(fooProperty)', + ], + [ + [ + new ArrayType(new StringType(), new StringType()), + new HasOffsetType(new ConstantStringType('a')), + ], + IntersectionType::class, + 'non-empty-array&hasOffset(\'a\')', + ], + [ + [ new ArrayType(new StringType(), new StringType()), - new HasOffsetType(new StringType()), - new HasOffsetType(new StringType()), + new HasOffsetType(new ConstantStringType('a')), + new HasOffsetType(new ConstantStringType('a')), + ], + IntersectionType::class, + 'non-empty-array&hasOffset(\'a\')', + ], + [ + [ + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new HasOffsetType(new ConstantStringType('a')), + ], + ConstantArrayType::class, + 'array{a: \'foo\'}', + ], + [ + [ + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new HasOffsetType(new ConstantStringType('b')), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ClosureType([], new MixedType(), false), + new HasOffsetType(new ConstantStringType('a')), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + TypeCombinator::union( + new ConstantArrayType( + [new ConstantStringType('a')], + [new ConstantStringType('foo')], + ), + new ConstantArrayType( + [new ConstantStringType('b')], + [new ConstantStringType('foo')], + ), + ), + new HasOffsetType(new ConstantStringType('b')), + ], + ConstantArrayType::class, + 'array{b: \'foo\'}', + ], + [ + [ + 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, + 'non-empty-array', + ], + [ + [ + new StringType(), + new NonEmptyArrayType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + new NonEmptyArrayType(), + ], + IntersectionType::class, + 'non-empty-array', + ], + [ + [ + TypeCombinator::union( + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantIntegerType(0), + ], [ + new StringType(), + ]), + ), + new NonEmptyArrayType(), + ], + ConstantArrayType::class, + 'array{string}', + ], + [ + [ + new ConstantArrayType([], []), + new NonEmptyArrayType(), + ], + NeverType::class, + '*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')), + ]), + ], + IntersectionType::class, + 'non-empty-array&hasOffset(\'bar\')&hasOffset(\'foo\')', + ], + [ + [ + new StringType(), + new IntegerType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new MixedType(false, new StringType()), + new StringType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new MixedType(false, new StringType()), + new ConstantStringType('foo'), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new MixedType(false, new StringType()), + new ConstantIntegerType(1), + ], + ConstantIntegerType::class, + '1', + ], + [ + [ + new MixedType(false, new StringType()), + new MixedType(false, new IntegerType()), + ], + MixedType::class, + 'mixed~(int|string)=implicit', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('DateTime'), + ], + IntersectionType::class, + 'DateTime&T (function a(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('DateTime'), + ], + TemplateObjectType::class, + 'T of DateTime (function a(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + ], + TemplateType::class, + 'T of DateTime (function a(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'U', + new ObjectType('DateTime'), + TemplateTypeVariance::createInvariant(), + ), + ], + IntersectionType::class, + 'T of DateTime (function a(), parameter)&U of DateTime (function a(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new MixedType(), + ], + TemplateType::class, + 'T (function a(), parameter)=explicit', + ], + [ + [ + new StringType(), + new ClassStringType(), + ], + ClassStringType::class, + 'class-string', + ], + [ + [ + new ClassStringType(), + new ConstantStringType(stdClass::class), + ], + ConstantStringType::class, + '\'stdClass\'', + ], + [ + [ + new ClassStringType(), + new ConstantStringType('Nonexistent'), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ClassStringType(), + new IntegerType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + ], + ConstantStringType::class, + '\'Exception\'', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new ClassStringType(), + ], + GenericClassStringType::class, + 'class-string', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new StringType(), + ], + GenericClassStringType::class, + 'class-string', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + ], + GenericClassStringType::class, + 'class-string', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), + ], + GenericClassStringType::class, + 'class-string', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + ], + GenericClassStringType::class, + 'class-string', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), + ], + ConstantStringType::class, + '\'Exception\'', + ], + [ + [ + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), + ], + ConstantStringType::class, + '\'Exception\'', + ], + [ + [ + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), + ], + IntersectionType::class, + "'Exception'&class-string", + ], + [ + [ + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(2, 5), + ], + IntegerRangeType::class, + 'int<2, 3>', + ], + [ + [ + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(3, 5), + ], + ConstantIntegerType::class, + '3', + ], + [ + [ + IntegerRangeType::fromInterval(1, 3), + IntegerRangeType::fromInterval(7, 9), + ], + NeverType::class, + '*NEVER*=implicit', + ], + [ + [ + 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, - 'array&nonEmpty', + 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, - 'array&nonEmpty', + '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, - 'DateTime&T (function a(), parameter)', + 'numeric-string', ], [ [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() - ), - new ObjectType('DateTime'), + new IntegerType(), + new AccessoryNumericStringType(), ], - 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'), - 'T', - new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() - ), + new IntersectionType([ + new ArrayType(new StringType(), new IntegerType()), + new NonEmptyArrayType(), + ]), + new NeverType(), ], - TemplateType::class, - 'T of DateTime (function a(), parameter)', + NeverType::class, + '*NEVER*=implicit', ], [ [ 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'), - TemplateTypeVariance::createInvariant() + new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), ), + new UnionType([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, - TemplateTypeVariance::createInvariant() + new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), ), - new MixedType(), + new BenevolentUnionType([new IntegerType(), new StringType()]), ], - TemplateType::class, - 'T (function a(), parameter)=explicit', + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', ], [ [ - new StringType(), - new ClassStringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new UnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new UnionType([new IntegerType(), new StringType()]), ], - ClassStringType::class, - 'class-string', + UnionType::class, + 'T of int|string (function my_array_keys(), parameter)', ], [ [ - new ClassStringType(), - new ConstantStringType(\stdClass::class), + new MixedType(), + new StrictMixedType(), ], - ConstantStringType::class, - '\'stdClass\'', + StrictMixedType::class, + 'mixed', ], [ [ - new ClassStringType(), - new ConstantStringType('Nonexistent'), + new NeverType(true), + new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=explicit', ], [ [ - new ClassStringType(), + new NeverType(), new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - ConstantStringType::class, - '\'Exception\'', + IntersectionType::class, + 'lowercase-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ClassStringType(), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], - GenericClassStringType::class, - 'class-string', + IntersectionType::class, + 'lowercase-string&numeric-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new StringType(), + 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 GenericClassStringType(new ObjectType(\Exception::class)), + 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(\Throwable::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(\InvalidArgumentException::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(\stdClass::class)), + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'numeric-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - ConstantStringType::class, - '\'Exception\'', + IntersectionType::class, + 'non-falsy-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(\Throwable::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(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], - NeverType::class, - '*NEVER*', + IntersectionType::class, + 'literal-string&uppercase-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\stdClass::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 [ [ - [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(2, 5), - ], - IntegerRangeType::class, - 'int<2, 3>', + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(3, 5), - ], - ConstantIntegerType::class, - '3', + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType(stdClass::class, 'ONE'), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - IntegerRangeType::fromInterval(7, 9), - ], - NeverType::class, - '*NEVER*', + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - new ConstantIntegerType(3), - ], - ConstantIntegerType::class, - '3', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - new ConstantIntegerType(4), - ], - NeverType::class, - '*NEVER*', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - IntegerRangeType::fromInterval(1, 3), - new IntegerType(), - ], - IntegerRangeType::class, - 'int<1, 3>', + new ObjectType('PHPStan\Fixture\TestEnum'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), ], + ObjectType::class, + 'PHPStan\Fixture\TestEnum', + ]; + yield [ [ - [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(), new MixedType()), - ], - ObjectType::class, - 'Traversable', + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ [ - [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(), new MixedType()), - ], - ObjectType::class, - 'Traversable', + 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 EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ + [ + 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 [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + ], + IntersectionType::class, + 'non-falsy-string', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + new ConstantStringType('0'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ + [ + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + new ConstantStringType('0'), + ], + ConstantStringType::class, + "'0'", + ]; + + yield [ + [ + new HasOffsetValueType(new ConstantStringType('a'), new ConstantIntegerType(1)), + new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()), + ], + HasOffsetValueType::class, + 'hasOffsetValue(\'a\', 1)', + ]; + + 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\', 1)', + ]; + yield [ + [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), + ], + IntersectionType::class, + 'array&oversized-array', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectWithoutClassType(), + ], + ObjectShapeType::class, + 'object{}', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectType(stdClass::class), + ], + IntersectionType::class, + 'object{}&stdClass', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('foo'), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('foo'), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('bar'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new IntegerType()], []), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []), + ], + ObjectShapeType::class, + 'object{foo: 1}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new StringType()], []), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['bar' => new StringType()], []), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), + ], + IntersectionType::class, + 'object{foo: int}&Traversable', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\Foo::class), + ], + IntersectionType::class, + 'ObjectShapesAcceptance\Foo&object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), + ], + ObjectType::class, + 'ObjectShapesAcceptance\ClassWithFooIntProperty', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), + ], + PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass&object{foo: int}' : '*NEVER*=implicit', + ]; + yield [ + [ + new NeverType(true), + new NonAcceptingNeverType(), + ], + NonAcceptingNeverType::class, + 'never=explicit', + ]; + yield [ + [ + new UnionType([ + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + ]), + new NonEmptyArrayType(), + ], + UnionType::class, + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', + ]; + yield [ + [ + new ConstantArrayType([], []), + new NonEmptyArrayType(), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new NonEmptyArrayType(), + ], + ConstantArrayType::class, + 'array{a?: true, b: true}', + ]; + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + new NonEmptyArrayType(), + ], + IntersectionType::class, + 'non-empty-array{a?: true, c?: true}', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ClosureType::createPure(), + ], + ClosureType::class, + 'pure-Closure', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ [ - [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(), new MixedType(true)), - ], - IntersectionType::class, - 'iterable&Traversable', + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ [ - [ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new MixedType()), - ], - IntersectionType::class, - 'iterable&Traversable', + 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 ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new MixedType(true)), - ], - IntersectionType::class, - 'iterable&Traversable', + new UnionType([ + new ConstantArrayType([], []), + $xy, + $abxy, + ]), + new UnionType([ + $xy, + $abxy, + ]), ], + UnionType::class, + "array{'xy'}|array{0: 'ab', 1?: 'xy'}", + ]; + + yield [ [ - [ - new MixedType(), - new MixedType(), - ], - MixedType::class, - 'mixed=implicit', + new ConstantArrayType([], []), + new UnionType([ + $xy, + $abxy, + ]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ - new MixedType(true), - new MixedType(), - ], - MixedType::class, - 'mixed=explicit', + new ConstantArrayType([], []), + $abxy, ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ - new MixedType(true), - new MixedType(true), - ], - MixedType::class, - 'mixed=explicit', + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], + 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 ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], + ConstantStringType::class, + '\'foo\'', + ]; + + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() - ), - new ObjectWithoutClassType(), - ], - TemplateObjectWithoutClassType::class, - 'T of object (function a(), parameter)', + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + 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 ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), ], + ConstantStringType::class, + '\'FOO\'', + ]; + + $c = $reflectionProvider->getClass(C::class); + + yield [ [ - [ - TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - 'T', - new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() - ), - new MixedType(), - ], - TemplateObjectWithoutClassType::class, - 'T of object (function a(), parameter)', + 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 ConstantStringType('NonexistentClass'), - new ClassStringType(), - ], - NeverType::class, - '*NEVER*', + new GenericStaticType($c, [new StringType()], null, [TemplateTypeVariance::createCovariant()]), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ [ - [ - new ConstantStringType(\stdClass::class), - new ClassStringType(), - ], - ConstantStringType::class, - '\'stdClass\'', + 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 ObjectType(\DateTimeInterface::class), - new ObjectType(\Iterator::class), - ], - IntersectionType::class, - 'DateTimeInterface&Iterator', + new StaticType($c), + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ [ - [ - new ObjectType(\DateTimeInterface::class), - new GenericObjectType(\Iterator::class, [new MixedType(), new MixedType()]), - ], - IntersectionType::class, - 'DateTimeInterface&Iterator', + new GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectWithoutClassType(), ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + yield [ [ - [ - 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 GenericStaticType($c, [new IntegerType()], null, [TemplateTypeVariance::createCovariant()]), + new ObjectType($c->getName()), + ], + GenericStaticType::class, + 'static(PHPStan\Generics\FunctionsAssertType\C)', + ]; + + $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() . '=final', ]; } /** * @dataProvider dataIntersect - * @param \PHPStan\Type\Type[] $types - * @param string $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ public function testIntersect( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::intersect(...$types); @@ -2758,20 +4750,38 @@ 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); } /** * @dataProvider dataIntersect - * @param \PHPStan\Type\Type[] $types - * @param string $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ public function testIntersectInversed( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::intersect(...array_reverse($types)); @@ -2783,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); } @@ -2794,7 +4822,7 @@ public function dataRemove(): array new ConstantBooleanType(true), new ConstantBooleanType(true), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new UnionType([ @@ -2850,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(), @@ -2874,7 +4902,43 @@ public function dataRemove(): array new BooleanType(), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', + ], + [ + StaticTypeFactory::falsey(), + StaticTypeFactory::falsey(), + NeverType::class, + '*NEVER*=implicit', + ], + [ + StaticTypeFactory::truthy(), + StaticTypeFactory::truthy(), + NeverType::class, + '*NEVER*=implicit', + ], + [ + StaticTypeFactory::truthy(), + StaticTypeFactory::falsey(), + MixedType::class, + 'mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', + ], + [ + StaticTypeFactory::falsey(), + StaticTypeFactory::truthy(), + UnionType::class, + '0|0.0|\'\'|\'0\'|array{}|false|null', + ], + [ + new BooleanType(), + StaticTypeFactory::falsey(), + ConstantBooleanType::class, + 'true', + ], + [ + new BooleanType(), + StaticTypeFactory::truthy(), + ConstantBooleanType::class, + 'false', ], [ new UnionType([ @@ -2929,17 +4993,17 @@ public function dataRemove(): array new IterableType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), ObjectType::class, - 'Traversable', + 'Traversable', ], [ new IterableType(new MixedType(), new MixedType()), - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), ArrayType::class, 'array', ], [ new IterableType(new MixedType(), new MixedType()), - new ObjectType(\Iterator::class), + new ObjectType(Iterator::class), IterableType::class, 'iterable', ], @@ -2959,25 +5023,25 @@ public function dataRemove(): array new BenevolentUnionType([new IntegerType(), new StringType()]), new ConstantStringType('foo'), UnionType::class, - 'int|string', + '(int|string)', ], [ new BenevolentUnionType([new IntegerType(), new StringType()]), new ConstantIntegerType(1), UnionType::class, - 'int<2, max>|int|string', + '(int|int<2, max>|string)', ], [ new BenevolentUnionType([new IntegerType(), new StringType()]), new UnionType([new IntegerType(), new StringType()]), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ArrayType(new MixedType(), new MixedType()), new ConstantArrayType([], []), IntersectionType::class, - 'array&nonEmpty', + 'non-empty-array', ], [ TypeCombinator::union( @@ -2986,11 +5050,11 @@ public function dataRemove(): array new ConstantIntegerType(0), ], [ new StringType(), - ]) + ]), ), new ConstantArrayType([], []), ConstantArrayType::class, - 'array(string)', + 'array{string}', ], [ new IntersectionType([ @@ -2999,13 +5063,13 @@ public function dataRemove(): array ]), new NonEmptyArrayType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType(), ConstantArrayType::class, - 'array()', + 'array{}', ], [ new ArrayType(new MixedType(), new MixedType()), @@ -3032,25 +5096,25 @@ 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), new MixedType(false, new StringType()), - NeverType::class, - '*NEVER*', + StringType::class, + 'string', ], [ new MixedType(false, new StringType()), @@ -3068,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')), @@ -3116,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), @@ -3164,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([ @@ -3176,10 +5240,10 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(1)), ConstantArrayType::class, - 'array(string)', + 'array{string}', ], [ new ConstantArrayType([ @@ -3188,30 +5252,88 @@ public function dataRemove(): array ], [ new StringType(), new StringType(), - ], 2, [1]), + ], [2], [1]), new HasOffsetType(new ConstantIntegerType(0)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', + ], + [ + new MixedType(), + new NeverType(), + MixedType::class, + 'mixed', + ], + [ + new ObjectWithoutClassType(), + new NeverType(), + ObjectWithoutClassType::class, + 'object', + ], + [ + new ObjectType(stdClass::class), + new NeverType(), + ObjectType::class, + 'stdClass', + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new BooleanType(), + TemplateTypeVariance::createInvariant(), + ), + new ConstantBooleanType(false), + TemplateMixedType::class, // should be TemplateConstantBooleanType + 'T (class Foo, parameter)', // should be T of true + ], + [ + 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}', ], ]; } /** * @dataProvider dataRemove - * @param \PHPStan\Type\Type $fromType - * @param \PHPStan\Type\Type $type - * @param string $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testRemove( Type $fromType, Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::remove($fromType, $type); - $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); } @@ -3231,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 4e4cea8ddc..46b5803374 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -2,16 +2,40 @@ namespace PHPStan\Type; +use DateTime; +use DateTimeImmutable; +use Exception; +use Iterator; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\HasMethodType; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasPropertyType; +use PHPStan\Type\Accessory\NonEmptyArrayType; 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; - -class UnionTypeTest extends \PHPStan\Testing\TestCase +use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use RecursionCallable\Foo; +use stdClass; +use function array_merge; +use function array_reverse; +use function get_class; +use function sprintf; +use const PHP_VERSION_ID; + +class UnionTypeTest extends PHPStanTestCase { public function dataIsCallable(): array @@ -21,9 +45,9 @@ public function dataIsCallable(): array TypeCombinator::union( new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], - [new ConstantStringType('Closure'), new ConstantStringType('bind')] + [new ConstantStringType('Closure'), new ConstantStringType('bind')], ), - new ConstantStringType('array_push') + new ConstantStringType('array_push'), ), TrinaryLogic::createYes(), ], @@ -53,8 +77,6 @@ public function dataIsCallable(): array /** * @dataProvider dataIsCallable - * @param UnionType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): void { @@ -62,11 +84,93 @@ public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): v $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsSuperTypeOf(): \Iterator + public function dataSelfCompare(): Iterator + { + $reflectionProvider = $this->createReflectionProvider(); + + $integerType = new IntegerType(); + $stringType = new StringType(); + $mixedType = new MixedType(); + $constantStringType = new ConstantStringType('foo'); + $constantIntegerType = new ConstantIntegerType(42); + $templateTypeScope = TemplateTypeScope::createWithClass('Foo'); + + $mixedParam = new NativeParameterReflection('foo', false, $mixedType, PassedByReference::createNo(), false, null); + $integerParam = new NativeParameterReflection('n', false, $integerType, PassedByReference::createNo(), false, null); + + yield [new AccessoryNumericStringType()]; + yield [new ArrayType($integerType, $stringType)]; + yield [new ArrayType($stringType, $mixedType)]; + yield [new BenevolentUnionType([$integerType, $stringType])]; + yield [new BooleanType()]; + yield [new CallableType()]; + 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 ConstantBooleanType(true)]; + yield [new ConstantFloatType(3.14)]; + yield [$constantIntegerType]; + yield [$constantStringType]; + yield [new ErrorType()]; + yield [new FloatType()]; + yield [new GenericClassStringType(new ObjectType(Exception::class))]; + yield [new GenericObjectType('Foo', [new ObjectType('DateTime')])]; + yield [new HasMethodType('Foo')]; + yield [new HasOffsetType($constantStringType)]; + yield [new HasPropertyType('foo')]; + yield [IntegerRangeType::fromInterval(3, 10)]; + yield [$integerType]; + yield [new IntersectionType([new HasMethodType('Foo'), new HasPropertyType('bar')])]; + yield [new IterableType($integerType, $stringType)]; + yield [$mixedType]; + yield [new NeverType()]; + yield [new NonEmptyArrayType()]; + yield [new NonexistentParentClassType()]; + yield [new NullType()]; + yield [new ObjectType('Foo')]; + yield [new ObjectWithoutClassType(new ObjectType('Foo'))]; + yield [new ResourceType()]; + yield [new StaticType($reflectionProvider->getClass('Foo'))]; + yield [new StrictMixedType()]; + yield [new StringAlwaysAcceptingObjectWithToStringType()]; + yield [$stringType]; + yield [TemplateTypeFactory::create($templateTypeScope, 'T', null, TemplateTypeVariance::createInvariant())]; + yield [TemplateTypeFactory::create($templateTypeScope, 'T', new ObjectType('Foo'), TemplateTypeVariance::createInvariant())]; + yield [TemplateTypeFactory::create($templateTypeScope, 'T', new ObjectWithoutClassType(), TemplateTypeVariance::createInvariant())]; + yield [new ThisType($reflectionProvider->getClass('Foo'))]; + yield [new UnionType([$integerType, $stringType])]; + yield [new VoidType()]; + } + + /** + * @dataProvider dataSelfCompare + * + */ + public function testSelfCompare(Type $type): void + { + $description = $type->describe(VerbosityLevel::precise()); + $this->assertTrue( + $type->equals($type), + sprintf('%s -> equals(itself)', $description), + ); + $this->assertEquals( + 'Yes', + $type->isSuperTypeOf($type)->describe(), + sprintf('%s -> isSuperTypeOf(itself)', $description), + ); + $this->assertInstanceOf( + get_class($type), + TypeCombinator::union($type, $type), + sprintf('%s -> union with itself is same type', $description), + ); + } + + public function dataIsSuperTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -215,7 +319,7 @@ public function dataIsSuperTypeOf(): \Iterator yield [ $unionTypeB, - new ObjectType('Foo'), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ]; @@ -242,13 +346,115 @@ public function dataIsSuperTypeOf(): \Iterator new IntersectionType([new StringType(), new CallableType()]), TrinaryLogic::createNo(), ]; + + yield 'is super type of template-of-union with same members' => [ + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'is super type of template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'maybe super type of template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ]; + + yield 'is super type of template-of-string equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'maybe super type of template-of-string sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ]; } /** * @dataProvider dataIsSuperTypeOf - * @param UnionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -256,11 +462,11 @@ public function testIsSuperTypeOf(UnionType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): \Iterator + public function dataIsSubTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -410,16 +616,13 @@ public function dataIsSubTypeOf(): \Iterator yield [ $unionTypeB, - new ObjectType('Foo'), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ]; } /** * @dataProvider dataIsSubTypeOf - * @param UnionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -427,15 +630,12 @@ public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $ $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param UnionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -443,7 +643,70 @@ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, Trinar $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), + ); + } + + 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())), ); } @@ -454,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([ @@ -472,13 +737,14 @@ public function dataDescribe(): array new ConstantFloatType(2.2), new NullType(), new ConstantStringType('10'), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new ConstantBooleanType(true), new ConstantStringType('foo'), new ConstantStringType('2'), 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', ], [ @@ -497,9 +763,10 @@ public function dataDescribe(): array new IntegerType(), new FloatType(), ]), - new ConstantStringType('aaa') + new ConstantStringType('aaa'), ), - '\'aaa\'|array(\'a\' => int|string, \'b\' => bool|float)', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', 'array|string', ], [ @@ -518,9 +785,10 @@ public function dataDescribe(): array new IntegerType(), new FloatType(), ]), - new ConstantStringType('aaa') + new ConstantStringType('aaa'), ), - '\'aaa\'|array(\'a\' => string, \'b\' => bool)|array(\'b\' => int, \'c\' => float)', + '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', + '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', 'array|string', ], [ @@ -539,9 +807,10 @@ public function dataDescribe(): array new IntegerType(), new FloatType(), ]), - new ConstantStringType('aaa') + new ConstantStringType('aaa'), ), - '\'aaa\'|array(\'a\' => string, \'b\' => bool)|array(\'c\' => int, \'d\' => float)', + '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', + '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', 'array|string', ], [ @@ -559,9 +828,10 @@ public function dataDescribe(): array new IntegerType(), new BooleanType(), new FloatType(), - ]) + ]), ), - 'array(0 => int|string, ?1 => bool, ?2 => float)', + 'array{int, bool, float}|array{string}', + 'array{int, bool, float}|array{string}', 'array', ], [ @@ -571,33 +841,121 @@ public function dataDescribe(): array new ConstantStringType('foooo'), ], [ new ConstantStringType('barrr'), - ]) + ]), ), - 'array()|array(\'foooo\' => \'barrr\')', + 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], + [ + TypeCombinator::union( + new IntegerType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + ), + 'int|numeric-string', + 'int|numeric-string', + 'int|string', + ], + [ + TypeCombinator::union( + IntegerRangeType::fromInterval(0, 4), + IntegerRangeType::fromInterval(6, 10), + ), + 'int<0, 4>|int<6, 10>', + 'int<0, 4>|int<6, 10>', + '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', + ], ]; } /** * @dataProvider dataDescribe - * @param Type $type - * @param string $expectedValueDescription - * @param string $expectedTypeOnlyDescription */ public function testDescribe( Type $type, + string $expectedPreciseDescription, string $expectedValueDescription, - string $expectedTypeOnlyDescription + string $expectedTypeOnlyDescription, ): void { - $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::precise())); + $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), @@ -665,25 +1023,290 @@ 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(), + new FloatType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'accepts template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'accepts template-of-union sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'maybe accepts template-of-union sub type of a union member (argument)' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts template-of-string equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'accepts template-of-string sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'maybe accepts template-of-string sub type of a union member (argument)' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts template-of-union containing a union member' => [ + new UnionType([ + new IntegerType(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts intersection with template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new ObjectType('Iterator'), + new ObjectType('IteratorAggregate'), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new ObjectType('Iterator'), + new ObjectType('IteratorAggregate'), + ]), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('Countable'), + ]), + TrinaryLogic::createYes(), + ], ]; } /** * @dataProvider dataAccepts - * @param UnionType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( UnionType $type, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), - $type->accepts($acceptedType, true)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + $type->accepts($acceptedType, true)->result->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } @@ -691,12 +1314,12 @@ public function dataHasMethod(): array { return [ [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new IntegerType()]), + new UnionType([new ObjectType(DateTimeImmutable::class), new IntegerType()]), 'format', TrinaryLogic::createMaybe(), ], [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new ObjectType(\DateTime::class)]), + new UnionType([new ObjectType(DateTimeImmutable::class), new ObjectType(DateTime::class)]), 'format', TrinaryLogic::createYes(), ], @@ -706,7 +1329,7 @@ public function dataHasMethod(): array TrinaryLogic::createNo(), ], [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new NullType()]), + new UnionType([new ObjectType(DateTimeImmutable::class), new NullType()]), 'format', TrinaryLogic::createMaybe(), ], @@ -715,17 +1338,306 @@ public function dataHasMethod(): array /** * @dataProvider dataHasMethod - * @param UnionType $type - * @param string $methodName - * @param TrinaryLogic $expectedResult */ public function testHasMethod( UnionType $type, string $methodName, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame($expectedResult->describe(), $type->hasMethod($methodName)->describe()); } + public function testSorting(): void + { + $types = [ + new ConstantBooleanType(false), + new ConstantBooleanType(true), + new ConstantIntegerType(-1), + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantFloatType(-1.0), + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + new ConstantStringType(''), + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantStringType('')], [new ConstantStringType('')]), + new IntegerType(), + IntegerRangeType::fromInterval(10, 20), + IntegerRangeType::fromInterval(30, 40), + new FloatType(), + new StringType(), + new ClassStringType(), + new MixedType(), + ]; + + $type1 = new UnionType($types); + $type2 = new UnionType(array_reverse($types)); + + $this->assertSame( + $type1->describe(VerbosityLevel::precise()), + $type2->describe(VerbosityLevel::precise()), + 'UnionType sorting always produces the same order', + ); + + $this->assertTrue( + $type1->equals($type2), + 'UnionType sorting always produces the same order', + ); + } + + /** + * @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/alias-collision1.php b/tests/PHPStan/Type/data/alias-collision1.php new file mode 100644 index 0000000000..a6fec9dfcf --- /dev/null +++ b/tests/PHPStan/Type/data/alias-collision1.php @@ -0,0 +1,6 @@ + | Foo */ + #[\ReturnTypeWillChange] public function getIterator(); } diff --git a/tests/PHPStan/Type/data/dependent-phpdocs.php b/tests/PHPStan/Type/data/dependent-phpdocs.php index df181a3608..66deecea49 100644 --- a/tests/PHPStan/Type/data/dependent-phpdocs.php +++ b/tests/PHPStan/Type/data/dependent-phpdocs.php @@ -8,5 +8,5 @@ interface Foo extends \IteratorAggregate public function addPages($pages); /** non-empty */ - public function getIterator(); + public function getIterator(): \Traversable; } diff --git a/tests/PHPStan/Type/data/static-type-test.php b/tests/PHPStan/Type/data/static-type-test.php new file mode 100644 index 0000000000..76a67956ec --- /dev/null +++ b/tests/PHPStan/Type/data/static-type-test.php @@ -0,0 +1,18 @@ +&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 $source of function token_get_all expects string, PhpParser\Node\Expr\MethodCall given.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Lexer.php')]['messages'][0]['message']); - $this->assertSame('Parameter #1 $code of 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(4, $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']); - - file_put_contents($serializerPath, $originalSerializerCode); - $this->runPhpstan(0); - $this->assertResultCache(__DIR__ . '/resultCache_1.php'); - } - - /** - * @param int $expectedExitCode - * @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 (\Nette\Utils\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(function (string $file): string { - return $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/anon-class/Granularity.php b/tests/e2e/anon-class/Granularity.php new file mode 100644 index 0000000000..9762130417 --- /dev/null +++ b/tests/e2e/anon-class/Granularity.php @@ -0,0 +1,16 @@ + 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: 2 - 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: "#^Comparison operation \"\\<\" between \\(array\\|float\\|int\\) and int results in an error\\.$#" - count: 1 - 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 @@ + - - - . - - - - - ../src - - - - - - - - - diff --git a/tests/phpunit.xsd b/tests/phpunit.xsd index 01ddc935fa..98af235c86 100644 --- a/tests/phpunit.xsd +++ b/tests/phpunit.xsd @@ -2,7 +2,7 @@ - This Schema file defines the rules by which the XML configuration file of PHPUnit 7.5 may be structured. + This Schema file defines the rules by which the XML configuration file of PHPUnit 9.5 may be structured. @@ -11,30 +11,33 @@ Root Element - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + @@ -122,39 +125,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -176,14 +146,22 @@ - - - + - + + + + + + + + + + + @@ -231,18 +209,16 @@ - + - - - + @@ -252,7 +228,10 @@ + + + @@ -262,7 +241,6 @@ - @@ -270,20 +248,22 @@ + - + + - - + + @@ -304,4 +284,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +